Private
Public Access
1
0

Compare commits

1 Commits

Author SHA1 Message Date
gauvainboiche
c7ad5898e6 feat(email): Adding email options for registration 2026-03-31 11:35:12 +02:00
61 changed files with 16104 additions and 47 deletions

View File

@@ -15,9 +15,17 @@ POSTGRES_USERS_USER=users
POSTGRES_USERS_PASSWORD=CHANGE_ME POSTGRES_USERS_PASSWORD=CHANGE_ME
POSTGRES_USERS_DB=star_wars_users POSTGRES_USERS_DB=star_wars_users
# ── Admin ────────────────────────────────────────────────────────────────────
# Password to unlock the team-switching debug widget in the UI
ADMIN_PASSWORD=CHANGE_ME
# ── CORS ───────────────────────────────────────────────────────────────────── # ── CORS ─────────────────────────────────────────────────────────────────────
CORS_ORIGIN=* CORS_ORIGIN=*
# ── Email (SMTP) ──────────────────────────────────────────────────────────────
# In development the Mailpit container is used automatically (no config needed).
# For production, set these to your real SMTP provider (e.g. Brevo, Mailgun, etc.)
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Star Wars Wild Space <noreply@wildspace.local>
# Public URL of the app — used to build links in emails
APP_URL=http://localhost:8080

View File

@@ -35,6 +35,16 @@ services:
retries: 15 retries: 15
start_period: 5s start_period: 5s
mailpit:
image: axllent/mailpit:latest
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI (open http://localhost:8025 to read emails)
environment:
MP_MAX_MESSAGES: 500
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
app: app:
build: . build: .
ports: ports:
@@ -46,6 +56,13 @@ services:
ADMIN_PASSWORD: ${ADMIN_PASSWORD} ADMIN_PASSWORD: ${ADMIN_PASSWORD}
PORT: "${PORT:-8080}" PORT: "${PORT:-8080}"
CONFIG_FILE_PATH: /app/config/game.settings.json CONFIG_FILE_PATH: /app/config/game.settings.json
SMTP_HOST: ${SMTP_HOST:-mailpit}
SMTP_PORT: ${SMTP_PORT:-1025}
SMTP_SECURE: ${SMTP_SECURE:-false}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-Star Wars Wild Space <noreply@wildspace.local>}
APP_URL: ${APP_URL:-http://localhost:8080}
volumes: volumes:
- ./config:/app/config - ./config:/app/config
depends_on: depends_on:
@@ -53,3 +70,5 @@ services:
condition: service_healthy condition: service_healthy
users_db: users_db:
condition: service_healthy condition: service_healthy
mailpit:
condition: service_started

9
node_modules/.package-lock.json generated vendored
View File

@@ -641,6 +641,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/nodemailer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

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

9
node_modules/nodemailer/.ncurc.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
'use strict';
module.exports = {
upgrade: true,
reject: [
// API changes break existing tests
'proxy'
]
};

8
node_modules/nodemailer/.prettierignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
coverage
*.min.js
dist
build
.nyc_output
package-lock.json
CHANGELOG.md

12
node_modules/nodemailer/.prettierrc generated vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"printWidth": 140,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

10
node_modules/nodemailer/.prettierrc.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
'use strict';
module.exports = {
printWidth: 160,
tabWidth: 4,
singleQuote: true,
endOfLine: 'lf',
trailingComma: 'none',
arrowParens: 'avoid'
};

9
node_modules/nodemailer/.release-please-config.json generated vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"packages": {
".": {
"release-type": "node",
"package-name": "nodemailer",
"pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]"
}
}
}

983
node_modules/nodemailer/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,983 @@
# CHANGELOG
## [8.0.4](https://github.com/nodemailer/nodemailer/compare/v8.0.3...v8.0.4) (2026-03-25)
### Bug Fixes
* sanitize envelope size to prevent SMTP command injection ([2d7b971](https://github.com/nodemailer/nodemailer/commit/2d7b9710e63555a1eb13d721296c51186d4b5651))
## [8.0.3](https://github.com/nodemailer/nodemailer/compare/v8.0.2...v8.0.3) (2026-03-18)
### Bug Fixes
* clean up addressparser and fix group name fallback producing undefined ([9d55877](https://github.com/nodemailer/nodemailer/commit/9d55877f8ed15a6aefd7ba76cbb6b6a6cdbcc4fd))
* fix cookie bugs, remove dead code, and improve hot-path efficiency ([e8c8b92](https://github.com/nodemailer/nodemailer/commit/e8c8b92f46f2a82d06d49cc9a6ffc26067f68524))
* refactor smtp-connection for clarity and add Node.js 6 syntax compat test ([c5b48ea](https://github.com/nodemailer/nodemailer/commit/c5b48ea61c28eabf347972f4198a12cdab226ff7))
* remove familySupportCache that broke DNS resolution tests ([c803d90](https://github.com/nodemailer/nodemailer/commit/c803d901f195a21edbb2c276b2e116564467aaaa))
## [8.0.2](https://github.com/nodemailer/nodemailer/compare/v8.0.1...v8.0.2) (2026-03-09)
### Bug Fixes
* merge fragmented display names with unquoted commas in addressparser ([fe27f7f](https://github.com/nodemailer/nodemailer/commit/fe27f7fd57f7587d897274438da2f628ad0ad7d9))
## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07)
### Bug Fixes
* absorb TLS errors during socket teardown ([7f8dde4](https://github.com/nodemailer/nodemailer/commit/7f8dde41438c66b8311e888fa5f8c518fcaba6f1))
* absorb TLS errors during socket teardown ([381f628](https://github.com/nodemailer/nodemailer/commit/381f628d55e62bb3131bd2a452fa1ce00bc48aea))
* Add Gmail Workspace service configuration ([#1787](https://github.com/nodemailer/nodemailer/issues/1787)) ([dc97ede](https://github.com/nodemailer/nodemailer/commit/dc97ede417b3030b311771541b1f17f5ca76bcbf))
## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04)
### ⚠ BREAKING CHANGES
* Error code 'NoAuth' renamed to 'ENOAUTH'
### Bug Fixes
* add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75))
* centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5))
* harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47))
* improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091))
## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27)
### Bug Fixes
* downgrade transient connection error logs to warn level ([4c041db](https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f))
## [7.0.12](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12) (2025-12-22)
### Bug Fixes
* added support for REQUIRETLS ([#1793](https://github.com/nodemailer/nodemailer/issues/1793)) ([053ce6a](https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84))
* use 8bit encoding for message/rfc822 attachments ([adf8611](https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b))
## [7.0.11](https://github.com/nodemailer/nodemailer/compare/v7.0.10...v7.0.11) (2025-11-26)
### Bug Fixes
* prevent stack overflow DoS in addressparser with deeply nested groups ([b61b9c0](https://github.com/nodemailer/nodemailer/commit/b61b9c0cfd682b6f647754ca338373b68336a150))
## [7.0.10](https://github.com/nodemailer/nodemailer/compare/v7.0.9...v7.0.10) (2025-10-23)
### Bug Fixes
* Increase data URI size limit from 100KB to 50MB and preserve content type ([28dbf3f](https://github.com/nodemailer/nodemailer/commit/28dbf3fe129653f5756c150a98dc40593bfb2cfe))
## [7.0.9](https://github.com/nodemailer/nodemailer/compare/v7.0.8...v7.0.9) (2025-10-07)
### Bug Fixes
* **release:** Trying to fix release proecess by upgrading Node version in runner ([579fce4](https://github.com/nodemailer/nodemailer/commit/579fce4683eb588891613a6c9a00d8092e8c62d1))
## [7.0.8](https://github.com/nodemailer/nodemailer/compare/v7.0.7...v7.0.8) (2025-10-07)
### Bug Fixes
* **addressparser:** flatten nested groups per RFC 5322 ([8f8a77c](https://github.com/nodemailer/nodemailer/commit/8f8a77c67f0ba94ddf4e16c68f604a5920fb5d26))
## [7.0.7](https://github.com/nodemailer/nodemailer/compare/v7.0.6...v7.0.7) (2025-10-05)
### Bug Fixes
- **addressparser:** Fixed addressparser handling of quoted nested email addresses ([1150d99](https://github.com/nodemailer/nodemailer/commit/1150d99fba77280df2cfb1885c43df23109a8626))
- **dns:** add memory leak prevention for DNS cache ([0240d67](https://github.com/nodemailer/nodemailer/commit/0240d6795ded6d8008d102161a729f120b6d786a))
- **linter:** Updated eslint and created prettier formatting task ([df13b74](https://github.com/nodemailer/nodemailer/commit/df13b7487e368acded35e45d0887d23c89c9177a))
- refresh expired DNS cache on error ([#1759](https://github.com/nodemailer/nodemailer/issues/1759)) ([ea0fc5a](https://github.com/nodemailer/nodemailer/commit/ea0fc5a6633a3546f4b00fcf2f428e9ca732cdb6))
- resolve linter errors in DNS cache tests ([3b8982c](https://github.com/nodemailer/nodemailer/commit/3b8982c1f24508089a8757b74039000a4498b158))
## [7.0.6](https://github.com/nodemailer/nodemailer/compare/v7.0.5...v7.0.6) (2025-08-27)
### Bug Fixes
- **encoder:** avoid silent data loss by properly flushing trailing base64 ([#1747](https://github.com/nodemailer/nodemailer/issues/1747)) ([01ae76f](https://github.com/nodemailer/nodemailer/commit/01ae76f2cfe991c0c3fe80170f236da60531496b))
- handle multiple XOAUTH2 token requests correctly ([#1754](https://github.com/nodemailer/nodemailer/issues/1754)) ([dbe0028](https://github.com/nodemailer/nodemailer/commit/dbe00286351cddf012726a41a96ae613d30a34ee))
- ReDoS vulnerability in parseDataURI and \_processDataUrl ([#1755](https://github.com/nodemailer/nodemailer/issues/1755)) ([90b3e24](https://github.com/nodemailer/nodemailer/commit/90b3e24d23929ebf9f4e16261049b40ee4055a39))
## [7.0.5](https://github.com/nodemailer/nodemailer/compare/v7.0.4...v7.0.5) (2025-07-07)
### Bug Fixes
- updated well known delivery service list ([fa2724b](https://github.com/nodemailer/nodemailer/commit/fa2724b337eb8d8fdcdd788fe903980b061316b8))
## [7.0.4](https://github.com/nodemailer/nodemailer/compare/v7.0.3...v7.0.4) (2025-06-29)
### Bug Fixes
- **pools:** Emit 'clear' once transporter is idle and all connections are closed ([839e286](https://github.com/nodemailer/nodemailer/commit/839e28634c9a93ae4321f399a8c893bf487a09fa))
- **smtp-connection:** jsdoc public annotation for socket ([#1741](https://github.com/nodemailer/nodemailer/issues/1741)) ([c45c84f](https://github.com/nodemailer/nodemailer/commit/c45c84fe9b8e2ec5e0615ab02d4197473911ab3e))
- **well-known-services:** Added AliyunQiye ([bb9e6da](https://github.com/nodemailer/nodemailer/commit/bb9e6daffb632d7d8f969359859f88a138de3a48))
## [7.0.3](https://github.com/nodemailer/nodemailer/compare/v7.0.2...v7.0.3) (2025-05-08)
### Bug Fixes
- **attachments:** Set the default transfer encoding for message/rfc822 attachments as '7bit' ([007d5f3](https://github.com/nodemailer/nodemailer/commit/007d5f3f40908c588f1db46c76de8b64ff429327))
## [7.0.2](https://github.com/nodemailer/nodemailer/compare/v7.0.1...v7.0.2) (2025-05-04)
### Bug Fixes
- **ses:** Fixed structured from header ([faa9a5e](https://github.com/nodemailer/nodemailer/commit/faa9a5eafaacbaf85de3540466a04636e12729b3))
## [7.0.1](https://github.com/nodemailer/nodemailer/compare/v7.0.0...v7.0.1) (2025-05-04)
### Bug Fixes
- **ses:** Use formatted FromEmailAddress for SES emails ([821cd09](https://github.com/nodemailer/nodemailer/commit/821cd09002f16c20369cc728b9414c7eb99e4113))
## [7.0.0](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.0) (2025-05-03)
### ⚠ BREAKING CHANGES
- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features
### Features
- SESv2 SDK support, removed older SES SDK v2 and v3 , removed SES rate limiting and idling features ([15db667](https://github.com/nodemailer/nodemailer/commit/15db667af2d0a5ed835281cfdbab16ee73b5edce))
## [6.10.1](https://github.com/nodemailer/nodemailer/compare/v6.10.0...v6.10.1) (2025-02-06)
### Bug Fixes
- close correct socket ([a18062c](https://github.com/nodemailer/nodemailer/commit/a18062c04d0e05ca4357fbe8f0a59b690fa5391e))
## [6.10.0](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v6.10.0) (2025-01-23)
### Features
- **services:** add Seznam email service configuration ([#1695](https://github.com/nodemailer/nodemailer/issues/1695)) ([d1ae0a8](https://github.com/nodemailer/nodemailer/commit/d1ae0a86883ba6011a49a5bbdf076098e2e3637a))
### Bug Fixes
- **proxy:** Set error and timeout errors for proxied sockets ([aa0c99c](https://github.com/nodemailer/nodemailer/commit/aa0c99c8f25440bb3dc91f4f3448777c800604d7))
## [6.9.16](https://github.com/nodemailer/nodemailer/compare/v6.9.15...v6.9.16) (2024-10-28)
### Bug Fixes
- **addressparser:** Correctly detect if user local part is attached to domain part ([f2096c5](https://github.com/nodemailer/nodemailer/commit/f2096c51b92a69ecfbcc15884c28cb2c2f00b826))
## [6.9.15](https://github.com/nodemailer/nodemailer/compare/v6.9.14...v6.9.15) (2024-08-08)
### Bug Fixes
- Fix memory leak ([#1667](https://github.com/nodemailer/nodemailer/issues/1667)) ([baa28f6](https://github.com/nodemailer/nodemailer/commit/baa28f659641a4bc30360633673d851618f8e8bd))
- **mime:** Added GeoJSON closes [#1637](https://github.com/nodemailer/nodemailer/issues/1637) ([#1665](https://github.com/nodemailer/nodemailer/issues/1665)) ([79b8293](https://github.com/nodemailer/nodemailer/commit/79b8293ad557d36f066b4675e649dd80362fd45b))
## [6.9.14](https://github.com/nodemailer/nodemailer/compare/v6.9.13...v6.9.14) (2024-06-19)
### Bug Fixes
- **api:** Added support for Ethereal authentication ([56b2205](https://github.com/nodemailer/nodemailer/commit/56b22052a98de9e363f6c4d26d1512925349c3f3))
- **services.json:** Add Email Services Provider Feishu Mail (CN) ([#1648](https://github.com/nodemailer/nodemailer/issues/1648)) ([e9e9ecc](https://github.com/nodemailer/nodemailer/commit/e9e9ecc99b352948a912868c7912b280a05178c6))
- **services.json:** update Mailtrap host and port in well known ([#1652](https://github.com/nodemailer/nodemailer/issues/1652)) ([fc2c9ea](https://github.com/nodemailer/nodemailer/commit/fc2c9ea0b4c4f4e514143d2a138c9a23095fc827))
- **well-known-services:** Add Loopia in well known services ([#1655](https://github.com/nodemailer/nodemailer/issues/1655)) ([21a28a1](https://github.com/nodemailer/nodemailer/commit/21a28a18fc9fdf8e0e86ddd846e54641395b2cb6))
## [6.9.13](https://github.com/nodemailer/nodemailer/compare/v6.9.12...v6.9.13) (2024-03-20)
### Bug Fixes
- **tls:** Ensure servername for SMTP ([d66fdd3](https://github.com/nodemailer/nodemailer/commit/d66fdd3dccacc4bc79d697fe9009204cc8d4bde0))
## [6.9.12](https://github.com/nodemailer/nodemailer/compare/v6.9.11...v6.9.12) (2024-03-08)
### Bug Fixes
- **message-generation:** Escape single quote in address names ([4ae5fad](https://github.com/nodemailer/nodemailer/commit/4ae5fadeaac70ba91abf529fcaae65f829a39101))
## [6.9.11](https://github.com/nodemailer/nodemailer/compare/v6.9.10...v6.9.11) (2024-02-29)
### Bug Fixes
- **headers:** Ensure that Content-type is the bottom header ([c7cf97e](https://github.com/nodemailer/nodemailer/commit/c7cf97e5ecc83f8eee773359951df995c9945446))
## [6.9.10](https://github.com/nodemailer/nodemailer/compare/v6.9.9...v6.9.10) (2024-02-22)
### Bug Fixes
- **data-uri:** Do not use regular expressions for parsing data URI schemes ([12e65e9](https://github.com/nodemailer/nodemailer/commit/12e65e975d80efe6bafe6de4590829b3b5ebb492))
- **data-uri:** Moved all data-uri regexes to use the non-regex parseDataUri method ([edd5dfe](https://github.com/nodemailer/nodemailer/commit/edd5dfe5ce9b725f8b8ae2830797f65b2a2b0a33))
## [6.9.9](https://github.com/nodemailer/nodemailer/compare/v6.9.8...v6.9.9) (2024-02-01)
### Bug Fixes
- **security:** Fix issues described in GHSA-9h6g-pr28-7cqp. Do not use eternal matching pattern if only a few occurences are expected ([dd8f5e8](https://github.com/nodemailer/nodemailer/commit/dd8f5e8a4ddc99992e31df76bcff9c590035cd4a))
- **tests:** Use native node test runner, added code coverage support, removed grunt ([#1604](https://github.com/nodemailer/nodemailer/issues/1604)) ([be45c1b](https://github.com/nodemailer/nodemailer/commit/be45c1b299d012358d69247019391a02734d70af))
## [6.9.8](https://github.com/nodemailer/nodemailer/compare/v6.9.7...v6.9.8) (2023-12-30)
### Bug Fixes
- **punycode:** do not use native punycode module ([b4d0e0c](https://github.com/nodemailer/nodemailer/commit/b4d0e0c7cc4b15bc4d9e287f91d1bcaca87508b0))
## [6.9.7](https://github.com/nodemailer/nodemailer/compare/v6.9.6...v6.9.7) (2023-10-22)
### Bug Fixes
- **customAuth:** Do not require user and pass to be set for custom authentication schemes (fixes [#1584](https://github.com/nodemailer/nodemailer/issues/1584)) ([41d482c](https://github.com/nodemailer/nodemailer/commit/41d482c3f01e26111b06f3e46351b193db3fb5cb))
## [6.9.6](https://github.com/nodemailer/nodemailer/compare/v6.9.5...v6.9.6) (2023-10-09)
### Bug Fixes
- **inline:** Use 'inline' as the default Content Dispostion value for embedded images ([db32c93](https://github.com/nodemailer/nodemailer/commit/db32c93fefee527bcc239f13056e5d9181a4d8af))
- **tests:** Removed Node v12 from test matrix as it is not compatible with the test framework anymore ([7fe0a60](https://github.com/nodemailer/nodemailer/commit/7fe0a608ed6bcb70dc6b2de543ebfc3a30abf984))
## [6.9.5](https://github.com/nodemailer/nodemailer/compare/v6.9.4...v6.9.5) (2023-09-06)
### Bug Fixes
- **license:** Updated license year ([da4744e](https://github.com/nodemailer/nodemailer/commit/da4744e491f3a68f4f68e4073684370592630e01))
## 6.9.4 2023-07-19
- Renamed SendinBlue to Brevo
## 6.9.3 2023-05-29
- Specified license identifier (was defined as MIT, actual value MIT-0)
- If SMTP server disconnects with a message, process it and include as part of the response error
## 6.9.2 2023-05-11
- Fix uncaught exception on invalid attachment content payload
## 6.9.1 2023-01-27
- Fix base64 encoding for emoji bytes in encoded words
## 6.9.0 2023-01-12
- Do not throw if failed to resolve IPv4 addresses
- Include EHLO extensions in the send response
- fix sendMail function: callback should be optional
## 6.8.0 2022-09-28
- Add DNS timeout (huksley)
- add dns.REFUSED (lucagianfelici)
## 6.7.8 2022-08-11
- Allow to use multiple Reply-To addresses
## 6.7.7 2022-07-06
- Resolver fixes
## 6.7.5 2022-05-04
- No changes, pushing a new README to npmjs.org
## 6.7.4 2022-04-29
- Ensure compatibility with Node 18
- Replaced Travis with Github Actions
## 6.7.3 2022-03-21
- Typo fixes
- Added stale issue automation fir Github
- Add Infomaniak config to well known service (popod)
- Update Outlook/Hotmail host in well known services (popod)
- fix: DSN recipient gets ignored (KornKalle)
## 6.7.2 2021-11-26
- Fix proxies for account verification
## 6.7.1 2021-11-15
- fix verify on ses-transport (stanofsky)
## 6.7.0 2021-10-11
- Updated DNS resolving logic. If there are multiple responses for a A/AAAA record, then loop these randomly instead of only caching the first one
## 6.6.5 2021-09-23
- Replaced Object.values() and Array.flat() with polyfills to allow using Nodemailer in Node v6+
## 6.6.4 2021-09-22
- Better compatibility with IPv6-only SMTP hosts (oxzi)
- Fix ses verify for sdk v3 (hannesvdvreken)
- Added SECURITY.txt for contact info
## 6.6.3 2021-07-14
- Do not show passwords in SMTP transaction logs. All passwords used in logging are replaced by `"/* secret */"`
## 6.6.1 2021-05-23
- Fixed address formatting issue where newlines in an email address, if provided via address object, were not properly removed. Reported by tmazeika (#1289)
## 6.6.0 2021-04-28
- Added new option `newline` for MailComposer
- aws ses connection verification (Ognjen Jevremovic)
## 6.5.0 2021-02-26
- Pass through textEncoding to subnodes
- Added support for AWS SES v3 SDK
- Fixed tests
## 6.4.18 2021-02-11
- Updated README
## 6.4.17 2020-12-11
- Allow mixing attachments with caendar alternatives
## 6.4.16 2020-11-12
- Applied updated prettier formating rules
## 6.4.15 2020-11-06
- Minor changes in header key casing
## 6.4.14 2020-10-14
- Disabled postinstall script
## 6.4.13 2020-10-02
- Fix normalizeHeaderKey method for single node messages
## 6.4.12 2020-09-30
- Better handling of attachment filenames that include quote symbols
- Includes all information from the oath2 error response in the error message (Normal Gaussian) [1787f227]
## 6.4.11 2020-07-29
- Fixed escape sequence handling in address parsing
## 6.4.10 2020-06-17
- Fixed RFC822 output for MailComposer when using invalid content-type value. Mostly relevant if message attachments have stragne content-type values set.
## 6.4.7 2020-05-28
- Always set charset=utf-8 for Content-Type headers
- Catch error when using invalid crypto.sign input
## 6.4.6 2020-03-20
- fix: `requeueAttempts=n` should requeue `n` times (Patrick Malouin) [a27ed2f7]
## 6.4.4 2020-03-01
- Add `options.forceAuth` for SMTP (Patrick Malouin) [a27ed2f7]
## 6.4.3 2020-02-22
- Added an option to specify max number of requeues when connection closes unexpectedly (Igor Sechyn) [8a927f5a]
## 6.4.2 2019-12-11
- Fixed bug where array item was used with a potentially empty array
## 6.4.1 2019-12-07
- Fix processing server output with unterminated responses
## 6.4.0 2019-12-04
- Do not use auth if server does not advertise AUTH support [f419b09d]
- add dns.CONNREFUSED (Hiroyuki Okada) [5c4c8ca8]
## 6.3.1 2019-10-09
- Ignore "end" events because it might be "error" after it (dex4er) [72bade9]
- Set username and password on the connection proxy object correctly (UsamaAshraf) [250b1a8]
- Support more DNS errors (madarche) [2391aa4]
## 6.3.0 2019-07-14
- Added new option to pass a set of httpHeaders to be sent when fetching attachments. See [PR #1034](https://github.com/nodemailer/nodemailer/pull/1034)
## 6.2.1 2019-05-24
- No changes. It is the same as 6.2.0 that was accidentally published as 6.2.1 to npm
## 6.2.0 2019-05-24
- Added new option for addressparser: `flatten`. If true then ignores group names and returns a single list of all addresses
## 6.1.1 2019-04-20
- Fixed regression bug with missing smtp `authMethod` property
## 6.1.0 2019-04-06
- Added new message property `amp` for providing AMP4EMAIL content
## 6.0.0 2019-03-25
- SMTPConnection: use removeListener instead of removeAllListeners (xr0master) [ddc4af15]
Using removeListener should fix memory leak with Node.js streams
## 5.1.1 2019-01-09
- Added missing option argument for custom auth
## 5.1.0 2019-01-09
- Official support for custom authentication methods and examples (examples/custom-auth-async.js and examples/custom-auth-cb.js)
## 5.0.1 2019-01-09
- Fixed regression error to support Node versions lower than 6.11
- Added expiremental custom authentication support
## 5.0.0 2018-12-28
- Start using dns.resolve() instead of dns.lookup() for resolving SMTP hostnames. Might be breaking change on some environments so upgrade with care
- Show more logs for renewing OAuth2 tokens, previously it was not possible to see what actually failed
## 4.7.0 2018-11-19
- Cleaned up List-\* header generation
- Fixed 'full' return option for DSN (klaronix) [23b93a3b]
- Support promises `for mailcomposer.build()`
## 4.6.8 2018-08-15
- Use first IP address from DNS resolution when using a proxy (Limbozz) [d4ca847c]
- Return raw email from SES transport (gabegorelick) [3aa08967]
## 4.6.7 2018-06-15
- Added option `skipEncoding` to JSONTransport
## 4.6.6 2018-06-10
- Fixes mime encoded-word compatibility issue with invalid clients like Zimbra
## 4.6.5 2018-05-23
- Fixed broken DKIM stream in Node.js v10
- Updated error messages for SMTP responses to not include a newline
## 4.6.4 2018-03-31
- Readded logo author link to README that was accidentally removed a while ago
## 4.6.3 2018-03-13
- Removed unneeded dependency
## 4.6.2 2018-03-06
- When redirecting URL calls then do not include original POST content
## 4.6.1 2018-03-06
- Fixed Smtp connection freezing, when trying to send after close / quit (twawszczak) [73d3911c]
## 4.6.0 2018-02-22
- Support socks module v2 in addition to v1 [e228bcb2]
- Fixed invalid promise return value when using createTestAccount [5524e627]
- Allow using local addresses [8f6fa35f]
## 4.5.0 2018-02-21
- Added new message transport option `normalizeHeaderKey(key)=>normalizedKey` for custom header formatting
## 4.4.2 2018-01-20
- Added sponsors section to README
- enclose encodeURIComponent in try..catch to handle invalid urls
## 4.4.1 2017-12-08
- Better handling of unexpectedly dropping connections
## 4.4.0 2017-11-10
- Changed default behavior for attachment option contentTransferEncoding. If it is unset then base64 encoding is used for the attachment. If it is set to false then previous default applies (base64 for most, 7bit for text)
## 4.3.1 2017-10-25
- Fixed a confict with Electron.js where timers do not have unref method
## 4.3.0 2017-10-23
- Added new mail object method `mail.normalize(cb)` that should make creating HTTP API based transports much easier
## 4.2.0 2017-10-13
- Expose streamed messages size and timers in info response
## v4.1.3 2017-10-06
- Allow generating preview links without calling createTestAccount first
## v4.1.2 2017-10-03
- No actual changes. Needed to push updated README to npmjs
## v4.1.1 2017-09-25
- Fixed JSONTransport attachment handling
## v4.1.0 2017-08-28
- Added new methods `createTestAccount` and `getTestMessageUrl` to use autogenerated email accounts from https://Ethereal.email
## v4.0.1 2017-04-13
- Fixed issue with LMTP and STARTTLS
## v4.0.0 2017-04-06
- License changed from EUPLv1.1 to MIT
## v3.1.8 2017-03-21
- Fixed invalid List-\* header generation
## v3.1.7 2017-03-14
- Emit an error if STARTTLS ends with connection being closed
## v3.1.6 2017-03-14
- Expose last server response for smtpConnection
## v3.1.5 2017-03-08
- Fixed SES transport, added missing `response` value
## v3.1.4 2017-02-26
- Fixed DKIM calculation for empty body
- Ensure linebreak after message content. This fixes DKIM signatures for non-multipart messages where input did not end with a newline
## v3.1.3 2017-02-17
- Fixed missing `transport.verify()` methods for SES transport
## v3.1.2 2017-02-17
- Added missing error handlers for Sendmail, SES and Stream transports. If a messages contained an invalid URL as attachment then these transports threw an uncatched error
## v3.1.1 2017-02-13
- Fixed missing `transport.on('idle')` and `transport.isIdle()` methods for SES transports
## v3.1.0 2017-02-13
- Added built-in transport for AWS SES. [Docs](http://localhost:1313/transports/ses/)
- Updated stream transport to allow building JSON strings. [Docs](http://localhost:1313/transports/stream/#json-transport)
- Added new method _mail.resolveAll_ that fetches all attachments and such to be able to more easily build API-based transports
## v3.0.2 2017-02-04
- Fixed a bug with OAuth2 login where error callback was fired twice if getToken was not available.
## v3.0.1 2017-02-03
- Fixed a bug where Nodemailer threw an exception if `disableFileAccess` option was used
- Added FLOSS [exception declaration](FLOSS_EXCEPTIONS.md)
## v3.0.0 2017-01-31
- Initial version of Nodemailer 3
This update brings a lot of breaking changes:
- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify <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-----

88
node_modules/nodemailer/eslint.config.js generated vendored Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
const globals = require('globals');
module.exports = [
{
ignores: ['node_modules/**', 'coverage/**', 'dist/**', 'build/**', '.nyc_output/**']
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2017,
sourceType: 'script',
globals: Object.assign({}, globals.node, globals.es2017, {
it: true,
describe: true,
beforeEach: true,
afterEach: true
})
},
rules: {
// Error detection
'for-direction': 'error',
'no-await-in-loop': 'error',
'no-div-regex': 'error',
eqeqeq: 'error',
'dot-notation': 'error',
curly: 'error',
'no-fallthrough': 'error',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true
}
],
'no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
],
'handle-callback-err': 'error',
'no-new': 'error',
'new-cap': 'error',
'no-eval': 'error',
'no-invalid-this': 'error',
radix: ['error', 'always'],
'no-use-before-define': ['error', 'nofunc'],
'callback-return': ['error', ['callback', 'cb', 'done']],
'no-regex-spaces': 'error',
'no-empty': 'error',
'no-duplicate-case': 'error',
'no-empty-character-class': 'error',
'no-redeclare': 'off', // Disabled per project preference
'block-scoped-var': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-void': 'error',
yoda: 'error',
'no-undef': 'error',
'global-require': 'error',
'no-var': 'error',
'no-bitwise': 'error',
'no-lonely-if': 'error',
'no-mixed-spaces-and-tabs': 'error',
'arrow-body-style': ['error', 'as-needed'],
'arrow-parens': ['error', 'as-needed'],
'prefer-arrow-callback': 'error',
'object-shorthand': 'error',
'prefer-spread': 'error',
'no-prototype-builtins': 'off', // Disabled per project preference
strict: ['error', 'global'],
// Disable all formatting rules (handled by Prettier)
indent: 'off',
quotes: 'off',
'linebreak-style': 'off',
semi: 'off',
'quote-props': 'off',
'comma-dangle': 'off',
'comma-style': 'off'
}
}
];

382
node_modules/nodemailer/lib/addressparser/index.js generated vendored Normal file
View File

@@ -0,0 +1,382 @@
'use strict';
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @param {Number} depth Current recursion depth for nested group protection
* @return {Object} Address object
*/
function _handleAddress(tokens, depth) {
let isGroup = false;
let state = 'text';
const addresses = [];
const data = {
address: [],
comment: [],
group: [],
text: [],
textWasQuoted: []
};
let insideQuotes = false;
// Filter out <addresses>, (comments) and regular text
for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i];
const prevToken = i ? tokens[i - 1] : null;
if (token.type === 'operator') {
switch (token.value) {
case '<':
state = 'address';
insideQuotes = false;
break;
case '(':
state = 'comment';
insideQuotes = false;
break;
case ':':
state = 'group';
isGroup = true;
insideQuotes = false;
break;
case '"':
insideQuotes = !insideQuotes;
state = 'text';
break;
default:
state = 'text';
insideQuotes = false;
break;
}
} else if (token.value) {
if (state === 'address') {
// Handle unquoted name that includes a "<".
// Apple Mail truncates everything between an unexpected < and an address.
token.value = token.value.replace(/^[^<]*<\s*/, '');
}
if (prevToken && prevToken.noBreak && data[state].length) {
data[state][data[state].length - 1] += token.value;
if (state === 'text' && insideQuotes) {
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
}
} else {
data[state].push(token.value);
if (state === 'text') {
data.textWasQuoted.push(insideQuotes);
}
}
}
}
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
let groupMembers = [];
if (data.group.length) {
const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
parsedGroup.forEach(member => {
if (member.group) {
groupMembers = groupMembers.concat(member.group);
} else {
groupMembers.push(member);
}
});
}
addresses.push({
name: data.text || '',
group: groupMembers
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
for (let i = data.text.length - 1; i >= 0; i--) {
// Security: Do not extract email addresses from quoted strings.
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
// Extracting emails from quoted text leads to misrouting vulnerabilities.
if (!data.textWasQuoted[i] && /^[^@\s]+@[^@\s]+$/.test(data.text[i])) {
data.address = data.text.splice(i, 1);
data.textWasQuoted.splice(i, 1);
break;
}
}
// Try a looser regex match if strict match found nothing
if (!data.address.length) {
let extracted = false;
for (let i = data.text.length - 1; i >= 0; i--) {
// Security: Do not extract email addresses from quoted strings
if (!data.textWasQuoted[i]) {
data.text[i] = data.text[i]
.replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, match => {
if (!extracted) {
data.address = [match.trim()];
extracted = true;
return ' ';
}
return match;
})
.trim();
if (extracted) {
break;
}
}
}
}
}
// If there's still no text but a comment exists, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurrence, 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(' ');
const address = {
address: data.address || data.text || '',
name: data.text || data.address || ''
};
if (address.address === address.name) {
if (/@/.test(address.address || '')) {
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() {
const list = [];
for (let i = 0, len = this.str.length; i < len; i++) {
const chr = this.str.charAt(i);
const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
this.checkChar(chr, nextChr);
}
this.list.forEach(node => {
node.value = (node.value || '').toString().trim();
if (node.value) {
list.push(node);
}
});
return list;
}
/**
* Checks if a character is an operator or text and acts accordingly
*
* @param {String} chr Character from the address field
*/
checkChar(chr, nextChr) {
if (this.escaped) {
// ignore next condition blocks
} else if (chr === this.operatorExpecting) {
this.node = {
type: 'operator',
value: chr
};
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
this.node.noBreak = true;
}
this.list.push(this.node);
this.node = null;
this.operatorExpecting = '';
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in this.operators) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = this.operators[chr];
this.escaped = false;
return;
} else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: 'text',
value: ''
};
this.list.push(this.node);
}
if (chr === '\n') {
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
chr = ' ';
}
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
// skip command bytes
this.node.value += chr;
}
this.escaped = false;
}
}
/**
* Maximum recursion depth for parsing nested groups.
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
* malicious input that could cause stack overflow.
*/
const MAX_NESTED_GROUP_DEPTH = 50;
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* 'Name <address@domain>'
*
* will be converted to
*
* [{name: 'Name', address: 'address@domain'}]
*
* @param {String} str Address field
* @param {Object} options Optional options object
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
* @return {Array} An array of address objects
*/
function addressparser(str, options) {
options = options || {};
const depth = options._depth || 0;
// Prevent stack overflow from deeply nested groups (DoS protection)
if (depth > MAX_NESTED_GROUP_DEPTH) {
return [];
}
const tokenizer = new Tokenizer(str);
const tokens = tokenizer.tokenize();
const 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(addr => {
const handled = _handleAddress(addr, depth);
if (handled.length) {
parsedAddresses = parsedAddresses.concat(handled);
}
});
// Merge fragments produced when unquoted display names contain commas.
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
// Recombine: a name-only entry followed by an entry with both name and address.
for (let i = parsedAddresses.length - 2; i >= 0; i--) {
const current = parsedAddresses[i];
const next = parsedAddresses[i + 1];
if (current.address === '' && current.name && !current.group && next.address && next.name) {
next.name = current.name + ', ' + next.name;
parsedAddresses.splice(i, 1);
}
}
if (options.flatten) {
const flatAddresses = [];
const walkAddressList = list => {
list.forEach(entry => {
if (entry.group) {
return walkAddressList(entry.group);
}
flatAddresses.push(entry);
});
};
walkAddressList(parsedAddresses);
return flatAddresses;
}
return parsedAddresses;
}
module.exports = addressparser;

140
node_modules/nodemailer/lib/base64/index.js generated vendored Normal file
View File

@@ -0,0 +1,140 @@
'use strict';
const { Transform } = require('stream');
/**
* 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;
}
const result = [];
let pos = 0;
const chunkLength = lineLength * 1024;
const wrapRegex = new RegExp('.{' + lineLength + '}', 'g');
while (pos < str.length) {
const wrappedLines = str.substr(pos, chunkLength).replace(wrapRegex, '$&\r\n');
result.push(wrappedLines);
pos += chunkLength;
}
return result.join('');
}
/**
* Creates a transform stream for encoding data to base64 encoding
*
* @constructor
* @param {Object} options Stream options
* @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
*/
class Encoder extends Transform {
constructor(options) {
super();
this.options = options || {};
if (this.options.lineLength !== false) {
this.options.lineLength = this.options.lineLength || 76;
}
this._curLine = '';
this._remainingBytes = false;
this.inputBytes = 0;
this.outputBytes = 0;
}
_transform(chunk, encoding, done) {
if (encoding !== 'buffer') {
chunk = Buffer.from(chunk, encoding);
}
if (!chunk || !chunk.length) {
return setImmediate(done);
}
this.inputBytes += chunk.length;
if (this._remainingBytes && this._remainingBytes.length) {
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
this._remainingBytes = false;
}
if (chunk.length % 3) {
this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3));
chunk = chunk.slice(0, chunk.length - (chunk.length % 3));
} else {
this._remainingBytes = false;
}
let b64 = this._curLine + encode(chunk);
if (this.options.lineLength) {
b64 = wrap(b64, this.options.lineLength);
const lastLF = b64.lastIndexOf('\n');
if (lastLF < 0) {
this._curLine = b64;
b64 = '';
} else {
this._curLine = b64.substring(lastLF + 1);
b64 = b64.substring(0, lastLF + 1);
if (b64 && !b64.endsWith('\r\n')) {
b64 += '\r\n';
}
}
} else {
this._curLine = '';
}
if (b64) {
this.outputBytes += b64.length;
this.push(Buffer.from(b64, 'ascii'));
}
setImmediate(done);
}
_flush(done) {
if (this._remainingBytes && this._remainingBytes.length) {
this._curLine += encode(this._remainingBytes);
}
if (this._curLine) {
this.outputBytes += this._curLine.length;
this.push(Buffer.from(this._curLine, 'ascii'));
this._curLine = '';
}
done();
}
}
module.exports = {
encode,
wrap,
Encoder
};

245
node_modules/nodemailer/lib/dkim/index.js generated vendored Normal file
View File

@@ -0,0 +1,245 @@
'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');
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();
}
const 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;
const signNextKey = () => {
if (keyPos >= this.keys.length) {
this.output.write(this.parser.rawHeaders);
return setImmediate(() => this.sendNextChunk());
}
const key = this.keys[keyPos++];
const dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
domainName: key.domainName,
keySelector: key.keySelector,
privateKey: key.privateKey,
headerFieldNames: this.options.headerFieldNames,
skipFields: this.options.skipFields
});
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) {
const 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.assign({}, extraOptions, this.options);
}
const signer = new DKIMSigner(options, this.keys, inputStream, output);
setImmediate(() => {
signer.signStream();
if (writeValue) {
setImmediate(() => {
inputStream.end(writeValue);
});
}
});
return output;
}
}
module.exports = DKIM;

154
node_modules/nodemailer/lib/dkim/message-parser.js generated vendored Normal file
View File

@@ -0,0 +1,154 @@
'use strict';
const { Transform } = require('stream');
/**
* 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) {
const lblen = this.lastBytes.length;
const nblen = Math.min(data.length, lblen);
// shift existing bytes
for (let i = 0, len = lblen - nblen; i < len; i++) {
this.lastBytes[i] = this.lastBytes[i + nblen];
}
// add new bytes
for (let i = 1; i <= nblen; i++) {
this.lastBytes[lblen - i] = data[data.length - i];
}
}
/**
* Finds and removes message headers from the remaining body. We want to keep
* headers separated until final delivery to be able to modify these
*
* @param {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;
}
const lblen = this.lastBytes.length;
let headerPos = 0;
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
let chr;
if (i < lblen) {
chr = this.lastBytes[i];
} else {
chr = data[i - lblen];
}
if (chr === 0x0a && i) {
const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
const pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
if (pr1 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
} else if (pr1 === 0x0d && pr2 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
}
}
if (this.headersParsed) {
this.headerChunks.push(data.slice(0, headerPos));
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
if (data.length - 1 > headerPos) {
const chunk = data.slice(headerPos);
this.bodySize += chunk.length;
// this would be the first chunk of data sent downstream
setImmediate(() => this.push(chunk));
}
return false;
}
this.headerBytes += data.length;
this.headerChunks.push(data);
// store last 4 bytes to catch header break
this.updateLastBytes(data);
return false;
}
_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) {
const chunk = Buffer.concat(this.headerChunks, this.headerBytes);
this.bodySize += chunk.length;
this.push(chunk);
this.headerChunks = null;
}
callback();
}
parseHeaders() {
const 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');
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--) {
const 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;

116
node_modules/nodemailer/lib/dkim/sign.js generated vendored Normal file
View File

@@ -0,0 +1,116 @@
'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
const defaultFieldNames =
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
'List-Owner:List-Archive';
const fieldNames = options.headerFieldNames || defaultFieldNames;
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
const signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
signer.update(canonicalizedHeaderData.headers);
let signature;
try {
signature = signer.sign(options.privateKey, 'base64');
} catch (_E) {
return false;
}
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
};
module.exports.relaxedHeaders = relaxedHeaders;
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
const 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) {
const includedFields = new Set();
const skip = new Set();
const headerFields = new Map();
(skipFields || '')
.toLowerCase()
.split(':')
.forEach(field => {
skip.add(field.trim());
});
(fieldNames || '')
.toLowerCase()
.split(':')
.filter(field => !skip.has(field.trim()))
.forEach(field => {
includedFields.add(field.trim());
});
for (let i = headers.length - 1; i >= 0; i--) {
const line = headers[i];
// only include the first value from bottom to top
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
headerFields.set(line.key, relaxedHeaderLine(line.line));
}
}
const headersList = [];
const fields = [];
includedFields.forEach(field => {
if (headerFields.has(field)) {
fields.push(field);
headersList.push(field + ':' + headerFields.get(field));
}
});
return {
headers: headersList.join('\r\n') + '\r\n',
fieldNames: fields.join(':')
};
}
function relaxedHeaderLine(line) {
return line
.substr(line.indexOf(':') + 1)
.replace(/\r?\n/g, '')
.replace(/\s+/g, ' ')
.trim();
}

58
node_modules/nodemailer/lib/errors.js generated vendored Normal file
View File

@@ -0,0 +1,58 @@
'use strict';
/**
* Nodemailer Error Codes
*
* Centralized error code definitions for consistent error handling.
*
* Usage:
* const errors = require('./errors');
* let err = new Error('Connection closed');
* err.code = errors.ECONNECTION;
*/
/**
* Error code descriptions for documentation and debugging
*/
const ERROR_CODES = {
// Connection errors
ECONNECTION: 'Connection closed unexpectedly',
ETIMEDOUT: 'Connection or operation timed out',
ESOCKET: 'Socket-level error',
EDNS: 'DNS resolution failed',
// TLS/Security errors
ETLS: 'TLS handshake or STARTTLS failed',
EREQUIRETLS: 'REQUIRETLS not supported by server (RFC 8689)',
// Protocol errors
EPROTOCOL: 'Invalid SMTP server response',
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
EMESSAGE: 'Message delivery error',
ESTREAM: 'Stream processing error',
// Authentication errors
EAUTH: 'Authentication failed',
ENOAUTH: 'Authentication credentials not provided',
EOAUTH2: 'OAuth2 token generation or refresh error',
// Resource errors
EMAXLIMIT: 'Pool resource limit reached (max messages per connection)',
// Transport-specific errors
ESENDMAIL: 'Sendmail command error',
ESES: 'AWS SES transport error',
// Configuration and access errors
ECONFIG: 'Invalid configuration',
EPROXY: 'Proxy connection error',
EFILEACCESS: 'File access rejected (disableFileAccess is set)',
EURLACCESS: 'URL access rejected (disableUrlAccess is set)',
EFETCH: 'HTTP fetch error'
};
// Export error codes as string constants and the full definitions object
module.exports = { ERROR_CODES };
for (const code of Object.keys(ERROR_CODES)) {
module.exports[code] = code;
}

276
node_modules/nodemailer/lib/fetch/cookies.js generated vendored Normal file
View File

@@ -0,0 +1,276 @@
'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) {
const urlparts = urllib.parse(url || '');
const cookie = this.parse(cookieStr);
let domain;
if (cookie.domain) {
domain = cookie.domain.replace(/^\./, '');
// do not allow cross origin cookies
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) {
const result = [];
for (let i = this.cookies.length - 1; i >= 0; i--) {
const cookie = this.cookies[i];
if (this.isExpired(cookie)) {
this.cookies.splice(i, 1);
continue;
}
if (this.match(cookie, url)) {
result.unshift(cookie);
}
}
return result;
}
/**
* Parses cookie string from the 'Set-Cookie:' header
*
* @param {String} cookieStr String from the 'Set-Cookie:' header
* @returns {Object} Cookie object
*/
parse(cookieStr) {
const cookie = {};
(cookieStr || '')
.toString()
.split(';')
.forEach(cookiePart => {
const valueParts = cookiePart.split('=');
const 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) {
const urlparts = urllib.parse(url || '');
// check if hostname matches
// .foo.com also matches subdomains, foo.com does not
if (
urlparts.hostname !== cookie.domain &&
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)
) {
return false;
}
// check if path matches
const 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) {
// nothing to do here
if (!cookie || !cookie.name) {
return false;
}
// overwrite if has same params
for (let i = 0, len = this.cookies.length; i < len; i++) {
if (this.compare(this.cookies[i], cookie)) {
// check if the cookie needs to be removed instead
if (this.isExpired(cookie)) {
this.cookies.splice(i, 1); // remove expired/unset cookie
return false;
}
this.cookies[i] = cookie;
return true;
}
}
// add as new if not already expired
if (!this.isExpired(cookie)) {
this.cookies.push(cookie);
}
return true;
}
/**
* Checks if two cookie objects are the same
*
* @param {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 === b.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;

278
node_modules/nodemailer/lib/fetch/index.js generated vendored Normal file
View File

@@ -0,0 +1,278 @@
'use strict';
const http = require('http');
const https = require('https');
const urllib = require('url');
const zlib = require('zlib');
const { PassThrough } = require('stream');
const Cookies = require('./cookies');
const packageData = require('../../package.json');
const net = require('net');
const errors = require('../errors');
const MAX_REDIRECTS = 5;
module.exports = function (url, options) {
return nmfetch(url, options);
};
module.exports.Cookies = Cookies;
function nmfetch(url, options) {
options = options || {};
options.fetchRes = options.fetchRes || new PassThrough();
options.cookies = options.cookies || new Cookies();
options.redirects = options.redirects || 0;
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
if (options.cookie) {
[].concat(options.cookie || []).forEach(cookie => {
options.cookies.set(cookie, url);
});
options.cookie = false;
}
const fetchRes = options.fetchRes;
const parsed = urllib.parse(url);
let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
let finished = false;
let cookies;
let body;
const handler = parsed.protocol === 'https:' ? https : http;
const headers = {
'accept-encoding': 'gzip,deflate',
'user-agent': 'nodemailer/' + packageData.version
};
Object.keys(options.headers || {}).forEach(key => {
headers[key.toLowerCase().trim()] = options.headers[key];
});
if (options.userAgent) {
headers['user-agent'] = options.userAgent;
}
if (parsed.auth) {
headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
}
if ((cookies = options.cookies.get(url))) {
headers.cookie = cookies;
}
if (options.body) {
if (options.contentType !== false) {
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
}
if (typeof options.body.pipe === 'function') {
// it's a stream
headers['Transfer-Encoding'] = 'chunked';
body = options.body;
body.on('error', err => {
if (finished) {
return;
}
finished = true;
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
});
} else {
if (options.body instanceof Buffer) {
body = options.body;
} else if (typeof options.body === 'object') {
try {
// encodeURIComponent can fail on invalid input (partial emoji etc.)
body = Buffer.from(
Object.keys(options.body)
.map(key => {
const value = options.body[key].toString().trim();
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
})
.join('&')
);
} catch (E) {
if (finished) {
return;
}
finished = true;
E.code = errors.EFETCH;
E.sourceUrl = url;
fetchRes.emit('error', E);
return;
}
} else {
body = Buffer.from(options.body.toString().trim());
}
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
headers['Content-Length'] = body.length;
}
// if method is not provided, use POST instead of GET
method = (options.method || '').toString().trim().toUpperCase() || 'POST';
}
let req;
const 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.assign(reqOptions, options.tls);
}
if (
parsed.protocol === 'https:' &&
parsed.hostname &&
parsed.hostname !== reqOptions.host &&
!net.isIP(parsed.hostname) &&
!reqOptions.servername
) {
reqOptions.servername = parsed.hostname;
}
try {
req = handler.request(reqOptions);
} catch (E) {
finished = true;
setImmediate(() => {
E.code = errors.EFETCH;
E.sourceUrl = url;
fetchRes.emit('error', E);
});
return fetchRes;
}
if (options.timeout) {
req.setTimeout(options.timeout, () => {
if (finished) {
return;
}
finished = true;
req.abort();
const err = new Error('Request Timeout');
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
});
}
req.on('error', err => {
if (finished) {
return;
}
finished = true;
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
});
req.on('response', res => {
let inflate;
if (finished) {
return;
}
switch (res.headers['content-encoding']) {
case 'gzip':
case 'deflate':
inflate = zlib.createUnzip();
break;
}
if (res.headers['set-cookie']) {
[].concat(res.headers['set-cookie'] || []).forEach(cookie => {
options.cookies.set(cookie, url);
});
}
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
// redirect
options.redirects++;
if (options.redirects > options.maxRedirects) {
finished = true;
const err = new Error('Maximum redirect count exceeded');
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
req.abort();
return;
}
// redirect does not include POST body
options.method = 'GET';
options.body = false;
return nmfetch(urllib.resolve(url, res.headers.location), options);
}
fetchRes.statusCode = res.statusCode;
fetchRes.headers = res.headers;
if (res.statusCode >= 300 && !options.allowErrorResponse) {
finished = true;
const err = new Error('Invalid status code ' + res.statusCode);
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
req.abort();
return;
}
res.on('error', err => {
if (finished) {
return;
}
finished = true;
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
req.abort();
});
if (inflate) {
res.pipe(inflate).pipe(fetchRes);
inflate.on('error', err => {
if (finished) {
return;
}
finished = true;
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
req.abort();
});
} else {
res.pipe(fetchRes);
}
});
setImmediate(() => {
if (body) {
try {
if (typeof body.pipe === 'function') {
return body.pipe(req);
}
req.write(body);
} catch (err) {
finished = true;
err.code = errors.EFETCH;
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;
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info(
{
tnx: 'send',
messageId
},
'Composing JSON structure of %s to <%s>',
messageId,
recipients.join(', ')
);
setImmediate(() => {
mail.normalize((err, data) => {
if (err) {
this.logger.error(
{
err,
tnx: 'send',
messageId
},
'Failed building JSON structure for %s. %s',
messageId,
err.message
);
return done(err);
}
delete data.envelope;
delete data.normalizedHeaders;
return done(null, {
envelope,
messageId,
message: this.options.skipEncoding ? data : JSON.stringify(data)
});
});
});
}
}
module.exports = JSONTransport;

599
node_modules/nodemailer/lib/mail-composer/index.js generated vendored Normal file
View File

@@ -0,0 +1,599 @@
/* eslint no-undefined: 0 */
'use strict';
const MimeNode = require('../mime-node');
const mimeFuncs = require('../mime-funcs');
const { parseDataURI } = require('../shared');
/**
* 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 => {
const key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
if (this.mail[key]) {
this.message.setHeader(header, this.mail[key]);
}
});
// Sets custom envelope
if (this.mail.envelope) {
this.message.setEnvelope(this.mail.envelope);
}
// ensure Message-Id value
this.message.messageId();
return this.message;
}
/**
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
*
* @param {Boolean} findRelated If true separate related attachments from attached ones
* @returns {Object} An object of arrays (`related` and `attached`)
*/
getAttachments(findRelated) {
let icalEvent, eventObject;
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
if (/^data:/i.test(attachment.path || attachment.href)) {
attachment = this._processDataUrl(attachment);
}
const contentType =
attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
const isImage = /^image\//i.test(contentType);
const isMessageNode = /^message\//i.test(contentType);
const contentDisposition =
attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
let contentTransferEncoding;
if ('contentTransferEncoding' in attachment) {
// also contains `false`, to set
contentTransferEncoding = attachment.contentTransferEncoding;
} else if (isMessageNode) {
// the content might include non-ASCII bytes but at this point we do not know it yet
contentTransferEncoding = '8bit';
} else {
contentTransferEncoding = 'base64'; // the default
}
const data = {
contentType,
contentDisposition,
contentTransferEncoding
};
if (attachment.filename) {
data.filename = attachment.filename;
} else if (!isMessageNode && attachment.filename !== false) {
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.assign({}, icalEvent);
eventObject.contentType = 'application/ics';
if (!eventObject.headers) {
eventObject.headers = {};
}
eventObject.filename = eventObject.filename || 'invite.ics';
eventObject.headers['Content-Disposition'] = 'attachment';
eventObject.headers['Content-Transfer-Encoding'] = 'base64';
}
if (!findRelated) {
return {
attached: attachments.concat(eventObject || []),
related: []
};
}
return {
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
related: attachments.filter(attachment => !!attachment.cid)
};
}
/**
* 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() {
const alternatives = [];
let 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.assign({}, icalEvent);
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 => {
if (/^data:/i.test(alternative.path || alternative.href)) {
alternative = this._processDataUrl(alternative);
}
const data = {
contentType:
alternative.contentType ||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
contentTransferEncoding: alternative.contentTransferEncoding
};
if (alternative.filename) {
data.filename = alternative.filename;
}
if (/^https?:\/\//i.test(alternative.path)) {
alternative.href = alternative.path;
alternative.path = undefined;
}
if (alternative.raw) {
data.raw = alternative.raw;
} else if (alternative.path) {
data.content = {
path: alternative.path
};
} else if (alternative.href) {
data.content = {
href: alternative.href
};
} 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) {
const node = parentNode
? parentNode.createChild('multipart/mixed', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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
});
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) {
const node = parentNode
? parentNode.createChild('multipart/alternative', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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
});
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) {
const node = parentNode
? parentNode.createChild('multipart/related; type="text/html"', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new 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
});
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 || '';
const encoding = (element.encoding || 'utf8')
.toString()
.toLowerCase()
.replace(/[-_\s]/g, '');
const node = parentNode
? parentNode.createChild(element.contentType, {
filename: element.filename,
textEncoding: this.mail.textEncoding,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: new 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
});
// add custom headers
if (element.headers) {
node.addHeader(element.headers);
}
if (element.cid) {
node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
}
if (element.contentTransferEncoding) {
node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
} else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
}
if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
node.setHeader(
'Content-Disposition',
element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment')
);
}
if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
element.content = Buffer.from(element.content, encoding);
}
// prefer pregenerated raw content
if (element.raw) {
node.setRaw(element.raw);
} else {
node.setContent(element.content);
}
return node;
}
/**
* Parses data uri and converts it to a Buffer
*
* @param {Object} element Content element
* @return {Object} Parsed element
*/
_processDataUrl(element) {
const dataUrl = element.path || element.href;
// Early validation to prevent ReDoS
if (!dataUrl || typeof dataUrl !== 'string') {
return element;
}
if (!dataUrl.startsWith('data:')) {
return element;
}
if (dataUrl.length > 52428800) {
// 52428800 chars = 50MB limit for data URL string (~37.5MB decoded image)
// Extract content type before rejecting to preserve MIME type
let detectedType = 'application/octet-stream';
const commaPos = dataUrl.indexOf(',');
if (commaPos > 0 && commaPos < 200) {
// Parse header safely with size limit
const header = dataUrl.substring(5, commaPos); // skip 'data:'
const parts = header.split(';');
if (parts[0] && parts[0].includes('/')) {
detectedType = parts[0].trim();
}
}
// Return empty content for excessively long data URLs
return Object.assign({}, element, {
path: false,
href: false,
content: Buffer.alloc(0),
contentType: element.contentType || detectedType
});
}
let parsedDataUri;
try {
parsedDataUri = parseDataURI(dataUrl);
} catch (_err) {
return element;
}
if (!parsedDataUri) {
return element;
}
element.content = parsedDataUri.data;
element.contentType = element.contentType || parsedDataUri.contentType;
if ('path' in element) {
element.path = false;
}
if ('href' in element) {
element.href = false;
}
return element;
}
}
module.exports = MailComposer;

446
node_modules/nodemailer/lib/mailer/index.js generated vendored Normal file
View File

@@ -0,0 +1,446 @@
'use strict';
const EventEmitter = require('events');
const shared = require('../shared');
const mimeTypes = require('../mime-funcs/mime-types');
const MailComposer = require('../mail-composer');
const DKIM = require('../dkim');
const httpProxyClient = require('../smtp-connection/http-proxy-client');
const errors = require('../errors');
const util = require('util');
const urllib = require('url');
const packageData = require('../../package.json');
const MailMessage = require('./mail-message');
const net = require('net');
const dns = require('dns');
const crypto = require('crypto');
/**
* Creates an object for exposing the Mail API
*
* @constructor
* @param {Object} transporter Transport object instance to pass the mails to
*/
class Mail extends EventEmitter {
constructor(transporter, options, defaults) {
super();
this.options = options || {};
this._defaults = defaults || {};
this._defaultPlugins = {
compile: [(...args) => this._convertDataImages(...args)],
stream: []
};
this._userPlugins = {
compile: [],
stream: []
};
this.meta = new Map();
this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false;
this.transporter = transporter;
this.transporter.mailer = this;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'mail'
});
this.logger.debug(
{
tnx: 'create'
},
'Creating transport: %s',
this.getVersionString()
);
// setup emit handlers for the transporter
if (typeof this.transporter.on === 'function') {
// deprecated log interface
this.transporter.on('log', log => {
this.logger.debug(
{
tnx: 'transport'
},
'%s: %s',
log.type,
log.message
);
});
// transporter errors
this.transporter.on('error', err => {
this.logger.error(
{
err,
tnx: 'transport'
},
'Transport Error: %s',
err.message
);
this.emit('error', err);
});
// indicates if the sender has became idle
this.transporter.on('idle', (...args) => {
this.emit('idle', ...args);
});
// indicates if the sender has became idle and all connections are terminated
this.transporter.on('clear', (...args) => {
this.emit('clear', ...args);
});
}
/**
* Optional methods passed to the underlying transport object
*/
['close', 'isIdle', 'verify'].forEach(method => {
this[method] = (...args) => {
if (typeof this.transporter[method] === 'function') {
if (method === 'verify' && typeof this.getSocket === 'function') {
this.transporter.getSocket = this.getSocket;
this.getSocket = false;
}
return this.transporter[method](...args);
}
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;
}
const 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 => {
const 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();
}
const userPlugins = this._userPlugins[step] || [];
const defaultPlugins = this._defaultPlugins[step] || [];
if (userPlugins.length) {
this.logger.debug(
{
tnx: 'transaction',
pluginCount: userPlugins.length,
step
},
'Using %s plugins for %s',
userPlugins.length,
step
);
}
if (userPlugins.length + defaultPlugins.length === 0) {
return callback();
}
let pos = 0;
let block = 'default';
const processPlugins = () => {
let curplugins = block === 'default' ? defaultPlugins : userPlugins;
if (pos >= curplugins.length) {
if (block === 'default' && userPlugins.length) {
block = 'user';
pos = 0;
curplugins = userPlugins;
} else {
return callback();
}
}
const plugin = curplugins[pos++];
plugin(mail, err => {
if (err) {
return callback(err);
}
processPlugins();
});
};
processPlugins();
}
/**
* Sets up proxy handler for a Nodemailer object
*
* @param {String} proxyUrl Proxy configuration url
*/
setupProxy(proxyUrl) {
const proxy = urllib.parse(proxyUrl);
// setup socket handler for the mailer object
this.getSocket = (options, callback) => {
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
if (this.meta.has('proxy_handler_' + protocol)) {
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
}
switch (protocol) {
// Connect using a HTTP CONNECT method
case 'http':
case 'https':
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
if (err) {
return callback(err);
}
return callback(null, {
connection: socket
});
});
return;
case 'socks':
case 'socks5':
case 'socks4':
case 'socks4a': {
if (!this.meta.has('proxy_socks_module')) {
let err = new Error('Socks module not loaded');
err.code = errors.EPROXY;
return callback(err);
}
const connect = ipaddress => {
const proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
const socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
const proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
const connectionOpts = {
proxy: {
ipaddress,
port: Number(proxy.port),
type: proxyType
},
[proxyV2 ? 'destination' : 'target']: {
host: options.host,
port: options.port
},
command: 'connect'
};
if (proxy.auth) {
const username = decodeURIComponent(proxy.auth.split(':').shift());
const password = decodeURIComponent(proxy.auth.split(':').pop());
if (proxyV2) {
connectionOpts.proxy.userId = username;
connectionOpts.proxy.password = password;
} else if (proxyType === 4) {
connectionOpts.userid = username;
} else {
connectionOpts.authentication = {
username,
password
};
}
}
socksClient.createConnection(connectionOpts, (err, info) => {
if (err) {
return callback(err);
}
return callback(null, {
connection: info.socket || info
});
});
};
if (net.isIP(proxy.hostname)) {
return connect(proxy.hostname);
}
return dns.resolve(proxy.hostname, (err, address) => {
if (err) {
return callback(err);
}
connect(Array.isArray(address) ? address[0] : address);
});
}
}
let err = new Error('Unknown proxy configuration');
err.code = errors.EPROXY;
callback(err);
};
}
_convertDataImages(mail, callback) {
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
return callback();
}
mail.resolveContent(mail.data, 'html', (err, html) => {
if (err) {
return callback(err);
}
let cidCounter = 0;
html = (html || '')
.toString()
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
const 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;

312
node_modules/nodemailer/lib/mailer/mail-message.js generated vendored Normal file
View File

@@ -0,0 +1,312 @@
'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 || {};
const options = mailer.options || {};
const defaults = mailer._defaults || {};
Object.assign(this.data, data);
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) {
const keys = [
[this.data, 'html'],
[this.data, 'text'],
[this.data, 'watchHtml'],
[this.data, 'amp'],
[this.data, 'icalEvent']
];
if (this.data.alternatives && this.data.alternatives.length) {
this.data.alternatives.forEach((alternative, i) => {
keys.push([this.data.alternatives, i]);
});
}
if (this.data.attachments && this.data.attachments.length) {
this.data.attachments.forEach((attachment, i) => {
if (!attachment.filename) {
attachment.filename =
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
if (attachment.filename.indexOf('.') < 0) {
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
}
}
if (!attachment.contentType) {
attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
}
keys.push([this.data.attachments, i]);
});
}
const mimeNode = new MimeNode();
const addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
addressKeys.forEach(address => {
let value;
if (this.message) {
value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
} else if (this.data[address]) {
value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
}
if (value && value.length) {
this.data[address] = value;
} else if (address in this.data) {
this.data[address] = null;
}
});
const singleKeys = ['from', 'sender'];
singleKeys.forEach(address => {
if (this.data[address]) {
this.data[address] = this.data[address].shift();
}
});
let pos = 0;
const resolveNext = () => {
if (pos >= keys.length) {
return callback(null, this.data);
}
const args = keys[pos++];
if (!args[0] || !args[0][args[1]]) {
return resolveNext();
}
shared.resolveContent(...args, (err, value) => {
if (err) {
return callback(err);
}
const node = {
content: value
};
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
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) {
const envelope = this.data.envelope || this.message.getEnvelope();
const messageId = this.message.messageId();
this.resolveAll((err, data) => {
if (err) {
return callback(err);
}
data.envelope = envelope;
data.messageId = messageId;
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
if (data[key] && data[key].content) {
if (typeof data[key].content === 'string') {
data[key] = data[key].content;
} else if (Buffer.isBuffer(data[key].content)) {
data[key] = data[key].content.toString();
}
}
});
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
data.icalEvent.content = data.icalEvent.content.toString('base64');
data.icalEvent.encoding = 'base64';
}
if (data.alternatives && data.alternatives.length) {
data.alternatives.forEach(alternative => {
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
alternative.content = alternative.content.toString('base64');
alternative.encoding = 'base64';
}
});
}
if (data.attachments && data.attachments.length) {
data.attachments.forEach(attachment => {
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
attachment.content = attachment.content.toString('base64');
attachment.encoding = 'base64';
}
});
}
data.normalizedHeaders = {};
Object.keys(data.headers || {}).forEach(key => {
let value = [].concat(data.headers[key] || []).shift();
value = (value && value.value) || value;
if (value) {
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
value = this.message._encodeHeaderValue(key, value);
}
data.normalizedHeaders[key] = value;
}
});
if (data.list && typeof data.list === 'object') {
const listHeaders = this._getListHeaders(data.list);
listHeaders.forEach(entry => {
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
});
}
if (data.references) {
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
}
if (data.inReplyTo) {
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
}
return callback(null, data);
});
}
setMailerHeader() {
if (!this.message || !this.data.xMailer) {
return;
}
this.message.setHeader('X-Mailer', this.data.xMailer);
}
setPriorityHeaders() {
if (!this.message || !this.data.priority) {
return;
}
switch ((this.data.priority || '').toString().toLowerCase()) {
case 'high':
this.message.setHeader('X-Priority', '1 (Highest)');
this.message.setHeader('X-MSMail-Priority', 'High');
this.message.setHeader('Importance', 'High');
break;
case 'low':
this.message.setHeader('X-Priority', '5 (Lowest)');
this.message.setHeader('X-MSMail-Priority', 'Low');
this.message.setHeader('Importance', 'Low');
break;
default:
// do not add anything, since all messages are 'Normal' by default
}
}
setListHeaders() {
if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
return;
}
// add optional List-* headers
this._getListHeaders(this.data.list).forEach(listHeader => {
listHeader.value.forEach(value => {
this.message.addHeader(listHeader.key, value);
});
});
}
_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;

610
node_modules/nodemailer/lib/mime-funcs/index.js generated vendored Normal file
View File

@@ -0,0 +1,610 @@
/* 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]/;
return typeof value === 'string' && !re.test(value);
},
/**
* 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;
const toCharset = 'UTF-8';
if (maxLength && maxLength > 7 + toCharset.length) {
maxLength -= 7 + toCharset.length;
}
if (mimeWordEncoding === 'Q') {
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
const ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (chr === ' ') {
return '_';
}
return '=' + (ord.length === 1 ? '0' + ord : ord);
});
} else if (mimeWordEncoding === 'B') {
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
const 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;
// find first word with a non-printable ascii or special symbol in it
const 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
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
if (!lastMatch) {
// should not happen
return value;
}
const startIndex =
firstMatch.index +
(
firstMatch[0].match(/[^\s]/) || {
index: 0
}
).index;
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
return (
(startIndex ? value.substr(0, startIndex) : '') +
this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : '')
);
},
/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
* @param {Object} structured Parsed header value
* @return {String} joined header value
*/
buildHeaderValue(structured) {
const paramsArray = [];
Object.keys(structured.params || {}).forEach(param => {
// filename might include unicode characters so it is a special case
// other values probably do not
const 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) {
const list = [];
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
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
const encodedStrArr = [];
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr.charAt(i);
ord = chr.charCodeAt(0);
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
chr += encodedStr.charAt(i + 1);
encodedStrArr.push(chr);
i++;
} else {
encodedStrArr.push(chr);
}
}
encodedStr = encodedStrArr;
}
// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = "utf-8''";
let encoded = true;
startPos = 0;
// process text with unicode or special chars
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr[i];
if (encoded) {
chr = this.safeEncodeURIComponent(chr);
} else {
// try to urlencode current char
chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr);
// By default it is not required to encode a line, the need
// only appears when the string contains unicode or special chars
// in this case we start processing the line over and encode all chars
if (chr !== encodedStr[i]) {
// Check if it is even possible to add the encoded char to the line
// If not, there is no reason to use this line, just push it to the list
// and start a new line with the char that needs encoding
if ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) {
list.push({
line,
encoded
});
line = '';
startPos = i - 1;
} else {
encoded = true;
i = startPos;
line = '';
continue;
}
}
}
// if the line is already too long, push it to the list and start a new one
if ((line + chr).length >= maxLength) {
list.push({
line,
encoded
});
line = chr = encodedStr[i] === ' ' ? ' ' : this.safeEncodeURIComponent(encodedStr[i]);
if (chr === encodedStr[i]) {
encoded = false;
startPos = i - 1;
} else {
encoded = true;
}
} else {
line += chr;
}
}
if (line) {
list.push({
line,
encoded
});
}
}
return list.map((item, i) => ({
// encoded lines: {name}*{part}*
// unencoded lines: {name}*{part}
// if any line needs to be encoded then the first line (part==0) is always encoded
key: key + '*' + i + (item.encoded ? '*' : ''),
value: item.line
}));
},
/**
* Parses a header value with key=value arguments into a structured
* object.
*
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
* {
* 'value': 'text/plain',
* 'params': {
* 'charset': 'UTF-8'
* }
* }
*
* @param {String} str Header value
* @return {Object} Header value as a parsed structure
*/
parseHeaderValue(str) {
const 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 => {
const c = s.charCodeAt(0).toString(16);
if (s === ' ') {
return '_';
}
return '%' + (c.length < 2 ? '0' : '') + c;
})
// change from urlencoding to percent encoding
.replace(/%/g, '=') +
'?=';
} else {
response.params[key] = value;
}
}
});
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;
const len = str.length;
let result = '';
let line, match;
while (pos < len) {
line = str.substr(pos, lineLength);
if (line.length < lineLength) {
result += line;
break;
}
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
line = match[0];
result += line;
pos += line.length;
continue;
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
}
result += line;
pos += line.length;
if (pos < len) {
result += '\r\n';
}
}
return result;
},
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
* @param {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) => {
const lines = [];
let curLine, match, chr, done;
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
maxlen = Math.max(maxlen || 0, 12);
while (str.length) {
curLine = str.substr(0, maxlen);
// move incomplete escaped char back to main
if ((match = curLine.match(/[=][0-9A-F]?$/i))) {
curLine = curLine.substr(0, match.index);
}
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));
}
};

2109
node_modules/nodemailer/lib/mime-funcs/mime-types.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1334
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');
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;

40
node_modules/nodemailer/lib/mime-node/le-unix.js generated vendored Normal file
View File

@@ -0,0 +1,40 @@
'use strict';
const { Transform } = require('stream');
/**
* Ensures that only <LF> is used for linebreaks
*
* @param {Object} options Stream options
*/
class LeUnix extends Transform {
constructor(options) {
super(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) {
// \r
buf = chunk.slice(lastPos, i);
lastPos = i + 1;
this.push(buf);
}
}
if (lastPos && lastPos < chunk.length) {
buf = chunk.slice(lastPos);
this.push(buf);
} else if (!lastPos) {
this.push(chunk);
}
done();
}
}
module.exports = LeUnix;

49
node_modules/nodemailer/lib/mime-node/le-windows.js generated vendored Normal file
View File

@@ -0,0 +1,49 @@
'use strict';
const { Transform } = require('stream');
/**
* Ensures that only <CR><LF> sequences are used for linebreaks
*
* @param {Object} options Stream options
*/
class LeWindows extends Transform {
constructor(options) {
super(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;

151
node_modules/nodemailer/lib/nodemailer.js generated vendored Normal file
View File

@@ -0,0 +1,151 @@
'use strict';
const Mailer = require('./mailer');
const shared = require('./shared');
const SMTPPool = require('./smtp-pool');
const SMTPTransport = require('./smtp-transport');
const SendmailTransport = require('./sendmail-transport');
const StreamTransport = require('./stream-transport');
const JSONTransport = require('./json-transport');
const SESTransport = require('./ses-transport');
const errors = require('./errors');
const nmfetch = require('./fetch');
const packageData = require('../package.json');
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
let testAccount = false;
module.exports.createTransport = function (transporter, defaults) {
let options;
if (
// provided transporter is a configuration object, not transporter plugin
(typeof transporter === 'object' && typeof transporter.send !== 'function') ||
// provided transporter looks like a connection url
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))
) {
const urlConfig = typeof transporter === 'string' ? transporter : transporter.url;
if (urlConfig) {
// parse a configuration URL into configuration options
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) {
const error = new Error(
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
);
error.code = errors.ECONFIG;
throw error;
}
transporter = new SESTransport(options);
} else {
transporter = new SMTPTransport(options);
}
}
return new Mailer(transporter, options, defaults);
};
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;
const chunks = [];
let chunklen = 0;
const requestHeaders = {};
const requestBody = {
requestor: packageData.name,
version: packageData.version
};
if (ETHEREAL_API_KEY) {
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
}
const 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', () => {
const res = Buffer.concat(chunks, chunklen);
let data;
try {
data = JSON.parse(res.toString());
} catch (E) {
return callback(E);
}
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;
}
const 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;

230
node_modules/nodemailer/lib/qp/index.js generated vendored Normal file
View File

@@ -0,0 +1,230 @@
'use strict';
const { Transform } = require('stream');
/**
* Encodes a Buffer into a Quoted-Printable encoded string
*
* @param {Buffer} buffer Buffer to convert
* @returns {String} Quoted-Printable encoded string
*/
// usable characters that do not need encoding
// https://tools.ietf.org/html/rfc2045#section-6.7
const QP_RANGES = [
[0x09], // <TAB>
[0x0a], // <LF>
[0x0d], // <CR>
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
];
function encode(buffer) {
if (typeof buffer === 'string') {
buffer = Buffer.from(buffer, 'utf-8');
}
let result = '';
let ord;
for (let i = 0, len = buffer.length; i < len; i++) {
ord = buffer[i];
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
if (
checkRanges(ord, QP_RANGES) &&
!((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))
) {
result += String.fromCharCode(ord);
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;
const len = str.length;
let match, code, line;
const lineMargin = Math.floor(lineLength / 3);
let result = '';
// insert soft linebreaks where needed
while (pos < len) {
line = str.substr(pos, lineLength);
if ((match = line.match(/\r\n/))) {
line = line.substr(0, match.index + match[0].length);
result += line;
pos += line.length;
continue;
}
if (line.substr(-1) === '\n') {
result += line;
pos += line.length;
continue;
}
if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
// truncate to nearest line break
line = line.substr(0, line.length - (match[0].length - 1));
result += line;
pos += line.length;
continue;
}
if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t.,!?][^ \t.,!?]*$/))) {
// truncate to nearest space
line = line.substr(0, line.length - (match[0].length - 1));
} else if (line.match(/[=][\da-f]{0,2}$/i)) {
// push incomplete encoding sequences to the next line
if ((match = line.match(/[=][\da-f]{0,1}$/i))) {
line = line.substr(0, line.length - match[0].length);
}
// ensure that utf-8 sequences are not split
while (
line.length > 3 &&
line.length < len - pos &&
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
(match = line.match(/[=][\da-f]{2}$/gi))
) {
code = parseInt(match[0].substr(1, 2), 16);
if (code < 128) {
break;
}
line = line.substr(0, line.length - 3);
if (code >= 0xc0) {
break;
}
}
}
if (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--) {
const range = ranges[i];
if (!range.length) {
continue;
}
if (range.length === 1 && nr === range[0]) {
return true;
}
if (range.length === 2 && nr >= range[0] && nr <= range[1]) {
return true;
}
}
return false;
}
/**
* 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();
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();
}
}
module.exports = {
encode,
wrap,
Encoder
};

205
node_modules/nodemailer/lib/sendmail-transport/index.js generated vendored Normal file
View File

@@ -0,0 +1,205 @@
'use strict';
const { spawn } = require('child_process');
const packageData = require('../../package.json');
const shared = require('../shared');
const errors = require('../errors');
/**
* Generates a Transport object for Sendmail
*
* Possible options can be the following:
*
* * **path** optional path to sendmail binary
* * **newline** either 'windows' or 'unix'
* * **args** an array of arguments for the sendmail binary
*
* @constructor
* @param {Object} optional config parameter for Sendmail
*/
class SendmailTransport {
constructor(options) {
options = options || {};
// use a reference to spawn for mocking purposes
this._spawn = spawn;
this.options = options;
this.name = 'Sendmail';
this.version = packageData.version;
this.path = 'sendmail';
this.args = false;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'sendmail'
});
if (typeof options === 'string') {
this.path = options;
} else if (typeof options === 'object') {
if (options.path) {
this.path = options.path;
}
if (Array.isArray(options.args)) {
this.args = options.args;
}
}
}
/**
* <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;
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
let returned;
const hasInvalidAddresses = []
.concat(envelope.from || [])
.concat(envelope.to || [])
.some(addr => /^-/.test(addr));
if (hasInvalidAddresses) {
const err = new Error('Can not send mail. Invalid envelope addresses.');
err.code = errors.ESENDMAIL;
return done(err);
}
// force -i to keep single dots
const args = this.args
? ['-i'].concat(this.args).concat(envelope.to)
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
const callback = err => {
if (returned) {
// ignore any additional responses, already done
return;
}
returned = true;
if (typeof done === 'function') {
if (err) {
return done(err);
}
return done(null, {
envelope,
messageId,
response: 'Messages queued for delivery'
});
}
};
let sendmail;
try {
sendmail = this._spawn(this.path, args);
} catch (E) {
this.logger.error(
{
err: E,
tnx: 'spawn',
messageId
},
'Error occurred while spawning sendmail. %s',
E.message
);
return callback(E);
}
if (sendmail) {
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();
}
const err = new Error(
code === 127 ? 'Sendmail command not found, process exited with code ' + code : 'Sendmail exited with code ' + code
);
err.code = errors.ESENDMAIL;
this.logger.error(
{
err,
tnx: 'stdin',
messageId
},
'Error sending message %s to sendmail. %s',
messageId,
err.message
);
callback(err);
});
sendmail.once('close', callback);
sendmail.stdin.on('error', err => {
this.logger.error(
{
err,
tnx: 'stdin',
messageId
},
'Error occurred when piping message %s to sendmail. %s',
messageId,
err.message
);
callback(err);
});
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info(
{
tnx: 'send',
messageId
},
'Sending message %s to <%s>',
messageId,
recipients.join(', ')
);
const sourceStream = mail.message.createReadStream();
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 {
const err = new Error('sendmail was not found');
err.code = errors.ESENDMAIL;
return callback(err);
}
}
}
module.exports = SendmailTransport;

223
node_modules/nodemailer/lib/ses-transport/index.js generated vendored Normal file
View File

@@ -0,0 +1,223 @@
'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 fromHeader = mail.message._headers.find(header => /^from$/i.test(header.key));
if (fromHeader) {
const mimeNode = new MimeNode('text/plain');
fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value));
}
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info(
{
tnx: 'send',
messageId
},
'Sending message %s to <%s>',
messageId,
recipients.join(', ')
);
const getRawMessage = next => {
// do not use Message-ID and Date in DKIM signature
if (!mail.data._dkim) {
mail.data._dkim = {};
}
if (mail.data._dkim.skipFields && typeof mail.data._dkim.skipFields === 'string') {
mail.data._dkim.skipFields += ':date:message-id';
} else {
mail.data._dkim.skipFields = 'date:message-id';
}
const sourceStream = mail.message.createReadStream();
const stream = sourceStream.pipe(new LeWindows());
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
let chunk;
while ((chunk = stream.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
sourceStream.once('error', err => stream.emit('error', err));
stream.once('error', err => next(err));
stream.once('end', () => next(null, Buffer.concat(chunks, chunklen)));
};
setImmediate(() =>
getRawMessage((err, raw) => {
if (err) {
this.logger.error(
{
err,
tnx: 'send',
messageId
},
'Failed creating message for %s. %s',
messageId,
err.message
);
return callback(err);
}
const sesMessage = Object.assign(
{
Content: {
Raw: {
// required
Data: raw // required
}
},
FromEmailAddress: fromHeader || envelope.from,
Destination: {
ToAddresses: envelope.to
}
},
mail.data.ses || {}
);
this.getRegion((err, region) => {
if (err || !region) {
region = 'us-east-1';
}
const command = new this.ses.SendEmailCommand(sesMessage);
const sendPromise = this.ses.sesClient.send(command);
sendPromise
.then(data => {
if (region === 'us-east-1') {
region = 'email';
}
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
);
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;

698
node_modules/nodemailer/lib/shared/index.js generated vendored Normal file
View File

@@ -0,0 +1,698 @@
/* eslint no-console: 0 */
'use strict';
const urllib = require('url');
const util = require('util');
const fs = require('fs');
const nmfetch = require('../fetch');
const dns = require('dns');
const net = require('net');
const os = require('os');
const DNS_TTL = 5 * 60 * 1000;
const CACHE_CLEANUP_INTERVAL = 30 * 1000; // Minimum 30 seconds between cleanups
const MAX_CACHE_SIZE = 1000; // Maximum number of entries in cache
let lastCacheCleanup = 0;
module.exports._lastCacheCleanup = () => lastCacheCleanup;
module.exports._resetCacheCleanup = () => {
lastCacheCleanup = 0;
};
let networkInterfaces;
try {
networkInterfaces = os.networkInterfaces();
} catch (_err) {
// fails on some systems
}
module.exports.networkInterfaces = networkInterfaces;
const isFamilySupported = (family, allowInternal) => {
const ifaces = module.exports.networkInterfaces;
if (!ifaces) {
// hope for the best
return true;
}
return Object.keys(ifaces)
.map(key => ifaces[key])
.reduce((acc, val) => acc.concat(val), [])
.filter(i => !i.internal || allowInternal)
.some(i => i.family === 'IPv' + family || i.family === family);
};
const resolve = (family, hostname, options, callback) => {
options = options || {};
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
return callback(null, []);
}
const dnsResolver = dns.Resolver ? new dns.Resolver(options) : dns;
dnsResolver['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 || {});
}
const addresses = value.addresses || [];
// Select a random address from available addresses, or null if none
const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
return Object.assign(
{
servername: value.servername,
host,
// Include all addresses for connection fallback support
_addresses: addresses
},
extra || {}
);
};
module.exports.resolveHostname = (options, callback) => {
options = options || {};
if (!options.host && options.servername) {
options.host = options.servername;
}
if (!options.host || net.isIP(options.host)) {
// nothing to do here
const value = {
addresses: [options.host],
servername: options.servername || false
};
return callback(
null,
formatDNSValue(value, {
cached: false
})
);
}
let cached;
if (dnsCache.has(options.host)) {
cached = dnsCache.get(options.host);
// Lazy cleanup with time throttling
const now = Date.now();
if (now - lastCacheCleanup > CACHE_CLEANUP_INTERVAL) {
lastCacheCleanup = now;
// Clean up expired entries
for (const [host, entry] of dnsCache.entries()) {
if (entry.expires && entry.expires < now) {
dnsCache.delete(host);
}
}
// If cache is still too large, remove oldest entries
if (dnsCache.size > MAX_CACHE_SIZE) {
const toDelete = Math.floor(MAX_CACHE_SIZE * 0.1); // Remove 10% of entries
const keys = Array.from(dnsCache.keys()).slice(0, toDelete);
keys.forEach(key => dnsCache.delete(key));
}
}
if (!cached.expires || cached.expires >= now) {
return callback(
null,
formatDNSValue(cached.value, {
cached: true
})
);
}
}
// Resolve both IPv4 and IPv6 addresses for fallback support
let ipv4Addresses = [];
let ipv6Addresses = [];
let ipv4Error = null;
let ipv6Error = null;
resolve(4, options.host, options, (err, addresses) => {
if (err) {
ipv4Error = err;
} else {
ipv4Addresses = addresses || [];
}
resolve(6, options.host, options, (err, addresses) => {
if (err) {
ipv6Error = err;
} else {
ipv6Addresses = addresses || [];
}
// Combine addresses: IPv4 first, then IPv6
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
if (allAddresses.length) {
const value = {
addresses: allAddresses,
servername: options.servername || options.host
};
dnsCache.set(options.host, {
value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(value, {
cached: false
})
);
}
// No addresses from resolve4/resolve6, try dns.lookup as fallback
if (ipv4Error && ipv6Error) {
// Both resolvers had errors
if (cached) {
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
cached: true,
error: ipv4Error
})
);
}
}
try {
dns.lookup(options.host, { all: true }, (err, addresses) => {
if (err) {
if (cached) {
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
cached: true,
error: err
})
);
}
return callback(err);
}
// Get all supported addresses from dns.lookup
const supportedAddresses = addresses
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
: [];
if (addresses && addresses.length && !supportedAddresses.length) {
// there are addresses but none can be used
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
}
if (!supportedAddresses.length && cached) {
// nothing was found, fallback to cached value
return callback(
null,
formatDNSValue(cached.value, {
cached: true
})
);
}
const value = {
addresses: supportedAddresses.length ? supportedAddresses : [options.host],
servername: options.servername || options.host
};
dnsCache.set(options.host, {
value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(value, {
cached: false
})
);
});
} catch (lookupErr) {
if (cached) {
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
cached: true,
error: lookupErr
})
);
}
return callback(ipv4Error || ipv6Error || lookupErr);
}
});
});
};
/**
* Parses connection url to a structured configuration object
*
* @param {String} str Connection url
* @return {Object} Configuration object
*/
module.exports.parseConnectionUrl = str => {
str = str || '';
const options = {};
const url = urllib.parse(str, true);
switch (url.protocol) {
case 'smtp:':
options.secure = false;
break;
case 'smtps:':
options.secure = true;
break;
case 'direct:':
options.direct = true;
break;
}
if (!isNaN(url.port) && Number(url.port)) {
options.port = Number(url.port);
}
if (url.hostname) {
options.host = url.hostname;
}
if (url.auth) {
const auth = url.auth.split(':');
options.auth = {
user: auth.shift(),
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) => {
const entry = Object.assign({}, defaults || {}, data || {});
delete entry.level;
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 || {};
const response = {};
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
if (!options.logger) {
// use vanity logger
levels.forEach(level => {
response[level] = () => false;
});
return response;
}
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
levels.forEach(level => {
response[level] = (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 () {
const args = Array.from(arguments);
const err = args.shift();
if (err) {
reject(err);
} else {
resolve(...args);
}
};
module.exports.parseDataURI = uri => {
if (typeof uri !== 'string') {
return null;
}
// Early return for non-data URIs to avoid unnecessary processing
if (!uri.startsWith('data:')) {
return null;
}
// Find the first comma safely - this prevents ReDoS
const commaPos = uri.indexOf(',');
if (commaPos === -1) {
return null;
}
const data = uri.substring(commaPos + 1);
const metaStr = uri.substring('data:'.length, commaPos);
let encoding;
const metaEntries = metaStr.split(';');
if (metaEntries.length > 0) {
const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
// Only recognize valid encoding types to prevent manipulation
if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
encoding = lastEntry;
metaEntries.pop();
}
}
const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
const params = {};
for (let i = 0; i < metaEntries.length; i++) {
const entry = metaEntries[i];
const sepPos = entry.indexOf('=');
if (sepPos > 0) {
// Ensure there's a key before the '='
const key = entry.substring(0, sepPos).trim();
const value = entry.substring(sepPos + 1).trim();
if (key) {
params[key] = value;
}
}
}
// Decode data based on encoding with proper error handling
let bufferData;
try {
if (encoding === 'base64') {
bufferData = Buffer.from(data, 'base64');
} else {
try {
bufferData = Buffer.from(decodeURIComponent(data));
} catch (_decodeError) {
bufferData = Buffer.from(data);
}
}
} catch (_bufferError) {
bufferData = Buffer.alloc(0);
}
return {
data: bufferData,
encoding: encoding || null,
contentType: contentType || 'application/octet-stream',
params
};
};
/**
* Resolves a String or a Buffer value for content value. Useful if the value
* is a Stream or a file or an URL. If the value is a Stream, overwrites
* the stream object with the resolved value (you can't stream a value twice).
*
* This is useful when you want to create a plugin that needs a content value,
* for example the `html` or `text` value as a String or a Buffer but not as
* a file path or an URL.
*
* @param {Object} data An object or an Array you want to resolve an element for
* @param {String|Number} key Property name or an Array index
* @param {Function} callback Callback function with (err, value)
*/
module.exports.resolveContent = (data, key, callback) => {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = module.exports.callbackPromise(resolve, reject);
});
}
let content = (data && data[key] && data[key].content) || data[key];
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
.toString()
.toLowerCase()
.replace(/[-_\s]/g, '');
if (!content) {
return callback(null, content);
}
if (typeof content === 'object') {
if (typeof content.pipe === 'function') {
return resolveStream(content, (err, value) => {
if (err) {
return callback(err);
}
// we can't stream twice the same content, so we need
// to replace the stream object with the streaming result
if (data[key].content) {
data[key].content = value;
} else {
data[key] = value;
}
callback(null, value);
});
} else if (/^https?:\/\//i.test(content.path || content.href)) {
return resolveStream(nmfetch(content.path || content.href), callback);
} else if (/^data:/i.test(content.path || content.href)) {
const 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 */) {
const args = Array.from(arguments);
const 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
target[key] = Object.assign(target[key] || {}, source[key]);
} 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;
}
const buf = Buffer.from(str);
let result = '';
for (let i = 0, len = buf.length; i < len; i++) {
const c = buf[i];
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
} else {
result += String.fromCharCode(c);
}
}
return result;
};
/**
* 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;
const 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) {
const levelMaxLen = levels.reduce((max, level) => Math.max(max, level.length), 0);
const levelNames = new Map();
levels.forEach(level => {
let levelName = level.toUpperCase();
if (levelName.length < levelMaxLen) {
levelName += ' '.repeat(levelMaxLen - levelName.length);
}
levelNames.set(level, levelName);
});
const print = (level, entry, message, ...args) => {
let prefix = '';
if (entry) {
if (entry.tnx === 'server') {
prefix = 'S: ';
} else if (entry.tnx === 'client') {
prefix = 'C: ';
}
if (entry.sid) {
prefix = '[' + entry.sid + '] ' + prefix;
}
if (entry.cid) {
prefix = '[#' + entry.cid + '] ' + prefix;
}
}
message = 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);
});
};
const logger = {};
levels.forEach(level => {
logger[level] = print.bind(null, level);
});
return logger;
}

View File

@@ -0,0 +1,105 @@
'use strict';
const { Transform } = require('stream');
/**
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
* Also makes sure that only <CR><LF> sequences are used for linebreaks
*
* @param {Object} options Stream options
*/
class DataStream extends Transform {
constructor(options) {
super(options);
this.options = options || {};
this.inByteCount = 0;
this.outByteCount = 0;
this.lastByte = false;
}
/**
* Escapes dots
*/
_transform(chunk, encoding, done) {
const chunks = [];
let chunklen = 0;
let i,
len,
lastPos = 0;
let buf;
if (!chunk || !chunk.length) {
return done();
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk);
}
this.inByteCount += chunk.length;
for (i = 0, len = chunk.length; i < len; i++) {
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) {
// \n
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
if (i > lastPos) {
buf = chunk.slice(lastPos, i);
chunks.push(buf);
chunklen += buf.length + 2;
} else {
chunklen += 2;
}
chunks.push(Buffer.from('\r\n'));
lastPos = i + 1;
}
}
}
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,144 @@
'use strict';
/**
* Minimal HTTP/S proxy client
*/
const net = require('net');
const tls = require('tls');
const urllib = require('url');
const errors = require('../errors');
/**
* Establishes proxied connection to destinationPort
*
* httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){
* socket.write("GET / HTTP/1.0\r\n\r\n");
* });
*
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
* @param {Number} destinationPort Port to open in destination host
* @param {String} destinationHost Destination hostname
* @param {Function} callback Callback to run with the rocket object once connection is established
*/
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
const proxy = urllib.parse(proxyUrl);
const options = {
host: proxy.hostname,
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
};
let connect;
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);
}
let socket;
// Error harness for initial connection. Once connection is established, the responsibility
// to handle errors is passed to whoever uses this socket
let finished = false;
const tempSocketErr = err => {
if (finished) {
return;
}
finished = true;
try {
socket.destroy();
} catch (_E) {
// ignore
}
callback(err);
};
const timeoutErr = () => {
const err = new Error('Proxy socket timed out');
err.code = 'ETIMEDOUT';
tempSocketErr(err);
};
socket = connect(options, () => {
if (finished) {
return;
}
const 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 = '';
const onSocketData = chunk => {
let match;
let remainder;
if (finished) {
return;
}
headers += chunk.toString('binary');
if ((match = headers.match(/\r\n\r\n/))) {
socket.removeListener('data', onSocketData);
remainder = headers.substr(match.index + match[0].length);
headers = headers.substr(0, match.index);
if (remainder) {
socket.unshift(Buffer.from(remainder, 'binary'));
}
// proxy connection is now established
finished = true;
// check response code
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
if (!match || (match[1] || '').charAt(0) !== '2') {
try {
socket.destroy();
} catch (_E) {
// ignore
}
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
err.code = errors.EPROXY;
return callback(err);
}
socket.removeListener('error', tempSocketErr);
socket.removeListener('timeout', timeoutErr);
socket.setTimeout(0);
return callback(null, socket);
}
};
socket.on('data', onSocketData);
});
socket.setTimeout(httpProxyClient.timeout || 30 * 1000);
socket.on('timeout', timeoutErr);
socket.once('error', tempSocketErr);
}
module.exports = httpProxyClient;

1906
node_modules/nodemailer/lib/smtp-connection/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

641
node_modules/nodemailer/lib/smtp-pool/index.js generated vendored Normal file
View File

@@ -0,0 +1,641 @@
'use strict';
const EventEmitter = require('events');
const PoolResource = require('./pool-resource');
const SMTPConnection = require('../smtp-connection');
const wellKnown = require('../well-known');
const shared = require('../shared');
const errors = require('../errors');
const packageData = require('../../package.json');
/**
* Creates a SMTP pool transport object for Nodemailer
*
* @constructor
* @param {Object} options SMTP Connection options
*/
class SMTPPool extends EventEmitter {
constructor(options) {
super();
options = options || {};
if (typeof options === 'string') {
options = {
url: options
};
}
let urlData;
let service = options.service;
if (typeof options.getSocket === 'function') {
this.getSocket = options.getSocket;
}
if (options.url) {
urlData = shared.parseConnectionUrl(options.url);
service = service || urlData.service;
}
this.options = shared.assign(
false, // create new object
options, // regular options
urlData, // url options
service && wellKnown(service) // wellknown options
);
this.options.maxConnections = this.options.maxConnections || 5;
this.options.maxMessages = this.options.maxMessages || 100;
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'smtp-pool'
});
this.name = 'SMTP (pool)';
this.version = packageData.version + '[client:' + packageData.version + ']';
this._rateLimit = {
counter: 0,
timeout: null,
waiting: [],
checkpoint: false,
delta: Number(this.options.rateDelta) || 1000,
limit: Number(this.options.rateLimit) || 0
};
this._closed = false;
this._queue = [];
this._connections = [];
this._connectionCounter = 0;
this.idling = true;
setImmediate(() => {
if (this.idling) {
this.emit('idle');
}
});
}
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param {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;
const 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
const invokeCallbacks = () => {
if (!this._queue.length) {
this.logger.debug(
{
tnx: 'connection'
},
'Pending queue entries cleared'
);
return;
}
const entry = this._queue.shift();
if (entry && typeof entry.callback === 'function') {
try {
entry.callback(new Error('Connection pool was closed'));
} catch (E) {
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() {
// do nothing if already closed
if (this._closed) {
return;
}
// do nothing if queue is empty
if (!this._queue.length) {
if (!this.idling) {
// no pending jobs
this.idling = true;
this.emit('idle');
}
return;
}
// find first available connection
let connection = this._connections.find(c => c.available);
if (!connection && this._connections.length < this.options.maxConnections) {
connection = this._createConnection();
}
if (!connection) {
// no more free connection slots available
this.idling = false;
return;
}
// check if there is free space in the processing queue
if (!this.idling && this._queue.length < this.options.maxConnections) {
this.idling = true;
this.emit('idle');
}
const entry = (connection.queueEntry = this._queue.shift());
entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
connection.available = false;
this.logger.debug(
{
tnx: 'pool',
cid: connection.id,
messageId: entry.messageId,
action: 'assign'
},
'Assigned message <%s> to #%s (%s)',
entry.messageId,
connection.id,
connection.messages + 1
);
if (this._rateLimit.limit) {
this._rateLimit.counter++;
if (!this._rateLimit.checkpoint) {
this._rateLimit.checkpoint = Date.now();
}
}
connection.send(entry.mail, (err, info) => {
// only process callback if current handler is not changed
if (entry === connection.queueEntry) {
try {
entry.callback(err, info);
} catch (E) {
this.logger.error(
{
err: E,
tnx: 'callback',
cid: connection.id
},
'Callback error for #%s: %s',
connection.id,
E.message
);
}
connection.queueEntry = false;
}
});
}
/**
* Creates a new pool resource
*/
_createConnection() {
const 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 !== errors.EMAXLIMIT) {
this.logger.warn(
{
err,
tnx: 'pool',
cid: connection.id
},
'Pool Error for #%s: %s',
connection.id,
err.message
);
} else {
this.logger.debug(
{
tnx: 'pool',
cid: connection.id,
action: 'maxlimit'
},
'Max messages limit exchausted for #%s',
connection.id
);
}
if (connection.queueEntry) {
try {
connection.queueEntry.callback(err);
} catch (E) {
this.logger.error(
{
err: E,
tnx: 'callback',
cid: connection.id
},
'Callback error for #%s: %s',
connection.id,
E.message
);
}
connection.queueEntry = false;
}
// remove the erroneus connection from connections list
this._removeConnection(connection);
this._continueProcessing();
});
connection.once('close', () => {
this.logger.info(
{
tnx: 'connection',
cid: connection.id,
action: 'closed'
},
'Connection #%s was closed',
connection.id
);
this._removeConnection(connection);
if (connection.queueEntry) {
// If the connection closed when sending, add the message to the queue again
// if max number of requeues is not reached yet
// Note that we must wait a bit.. because the callback of the 'error' handler might be called
// in the next event loop
setTimeout(() => {
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 += 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) {
const index = this._connections.indexOf(connection);
if (index !== -1) {
this._connections.splice(index, 1);
}
}
/**
* Checks if connections have hit current rate limit and if so, queues the availability callback
*
* @param {Function} callback Callback function to run once rate limiter has been cleared
*/
_checkRateLimit(callback) {
if (!this._rateLimit.limit) {
return callback();
}
const now = Date.now();
if (this._rateLimit.counter < this._rateLimit.limit) {
return callback();
}
this._rateLimit.waiting.push(callback);
if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) {
return this._clearRateLimit();
}
if (!this._rateLimit.timeout) {
this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint));
this._rateLimit.checkpoint = now;
}
}
/**
* Clears current rate limit limitation and runs paused callback
*/
_clearRateLimit() {
clearTimeout(this._rateLimit.timeout);
this._rateLimit.timeout = null;
this._rateLimit.counter = 0;
this._rateLimit.checkpoint = false;
// resume all paused connections
while (this._rateLimit.waiting.length) {
const cb = this._rateLimit.waiting.shift();
setImmediate(cb);
}
}
/**
* Returns true if there are free slots in the queue
*/
isIdle() {
return this.idling;
}
/**
* Verifies SMTP configuration
*
* @param {Function} callback Callback function
*/
verify(callback) {
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
callback = shared.callbackPromise(resolve, reject);
});
}
const 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 = Object.assign(shared.assign(false, options), socketOptions);
}
const 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'));
});
const 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) {
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;
connection.close();
return callback(err);
} else {
finalize();
}
});
});
return promise;
}
}
// expose to the world
module.exports = SMTPPool;

256
node_modules/nodemailer/lib/smtp-pool/pool-resource.js generated vendored Normal file
View File

@@ -0,0 +1,256 @@
'use strict';
const SMTPConnection = require('../smtp-connection');
const assign = require('../shared').assign;
const XOAuth2 = require('../xoauth2');
const errors = require('../errors');
const EventEmitter = require('events');
/**
* Creates an element for the pool
*
* @constructor
* @param {Object} options SMTPPool instance
*/
class PoolResource extends EventEmitter {
constructor(pool) {
super();
this.pool = pool;
this.options = pool.options;
this.logger = this.pool.logger;
if (this.options.auth) {
switch ((this.options.auth.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
const 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 = Object.assign(assign(false, options), socketOptions);
}
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;
const timer = setTimeout(() => {
if (returned) {
return;
}
// still have not returned, this means we have an unexpected connection close
const err = new Error('Unexpected socket close');
if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
// starttls connection errors
err.code = errors.ETLS;
}
callback(err);
}, 1000);
try {
timer.unref();
} catch (_E) {
// Ignore. Happens on envs with non-node timer implementation
}
});
this.connection.connect(() => {
if (returned) {
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);
});
}
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info(
{
tnx: 'send',
messageId,
cid: this.id
},
'Sending message %s using #%s to <%s>',
messageId,
this.id,
recipients.join(', ')
);
if (mail.data.dsn) {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
this.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(() => {
if (this.messages >= this.options.maxMessages) {
const err = new Error('Resource exhausted');
err.code = errors.EMAXLIMIT;
this.connection.close();
this.emit('error', err);
} else {
this.pool._checkRateLimit(() => {
this.available = true;
this.emit('available');
});
}
});
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;

402
node_modules/nodemailer/lib/smtp-transport/index.js generated vendored Normal file
View File

@@ -0,0 +1,402 @@
'use strict';
const EventEmitter = require('events');
const SMTPConnection = require('../smtp-connection');
const wellKnown = require('../well-known');
const shared = require('../shared');
const XOAuth2 = require('../xoauth2');
const errors = require('../errors');
const packageData = require('../../package.json');
/**
* Creates a SMTP transport object for Nodemailer
*
* @constructor
* @param {Object} options Connection options
*/
class SMTPTransport extends EventEmitter {
constructor(options) {
super();
options = options || {};
if (typeof options === 'string') {
options = {
url: options
};
}
let urlData;
let service = options.service;
if (typeof options.getSocket === 'function') {
this.getSocket = options.getSocket;
}
if (options.url) {
urlData = shared.parseConnectionUrl(options.url);
service = service || urlData.service;
}
this.options = shared.assign(
false, // create new object
options, // regular options
urlData, // url options
service && wellKnown(service) // wellknown options
);
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'smtp-transport'
});
this.name = 'SMTP';
this.version = packageData.version + '[client:' + packageData.version + ']';
if (this.options.auth) {
this.auth = this.getAuth({});
}
}
/**
* Placeholder function for creating proxy sockets. This method immediatelly returns
* without a socket
*
* @param {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;
}
const authData = Object.assign(
{},
this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {},
authOpts && typeof authOpts === 'object' ? authOpts : {}
);
if (Object.keys(authData).length === 0) {
return false;
}
switch ((authData.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
if (!authData.service && !authData.user) {
return false;
}
const oauth2 = new 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 = Object.assign(shared.assign(false, options), socketOptions);
}
const connection = new SMTPConnection(options);
connection.once('error', err => {
if (returned) {
return;
}
returned = true;
connection.close();
return callback(err);
});
connection.once('end', () => {
if (returned) {
return;
}
const timer = setTimeout(() => {
if (returned) {
return;
}
returned = true;
// still have not returned, this means we have an unexpected connection close
const err = new Error('Unexpected socket close');
if (connection && connection._socket && connection._socket.upgrading) {
// starttls connection errors
err.code = errors.ETLS;
}
callback(err);
}, 1000);
try {
timer.unref();
} catch (_E) {
// Ignore. Happens on envs with non-node timer implementation
}
});
const sendMessage = () => {
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
if (mail.data.dsn) {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
this.logger.info(
{
tnx: 'send',
messageId
},
'Sending message %s to <%s>',
messageId,
recipients.join(', ')
);
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
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;
}
const 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 = Object.assign(shared.assign(false, options), socketOptions);
}
const 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'));
});
const finalize = () => {
if (returned) {
return;
}
returned = true;
connection.quit();
return callback(null, true);
};
connection.connect(() => {
if (returned) {
return;
}
const 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) {
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;
connection.close();
return callback(err);
} else {
finalize();
}
});
});
return promise;
}
/**
* Releases resources
*/
close() {
if (this.auth && this.auth.oauth2) {
this.auth.oauth2.removeAllListeners();
}
this.emit('close');
}
}
// expose to the world
module.exports = SMTPTransport;

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;
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
this.logger.info(
{
tnx: 'send',
messageId
},
'Sending message %s to <%s> using %s line breaks',
messageId,
recipients.join(', '),
this.winbreak ? '<CR><LF>' : '<LF>'
);
setImmediate(() => {
let stream;
try {
stream = mail.message.createReadStream();
} catch (E) {
this.logger.error(
{
err: E,
tnx: 'send',
messageId
},
'Creating send stream failed for %s. %s',
messageId,
E.message
);
return done(E);
}
if (!this.options.buffer) {
stream.once('error', err => {
this.logger.error(
{
err,
tnx: 'send',
messageId
},
'Failed creating message for %s. %s',
messageId,
err.message
);
});
return done(null, {
envelope,
messageId,
message: stream
});
}
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
let chunk;
while ((chunk = stream.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
stream.once('error', err => {
this.logger.error(
{
err,
tnx: 'send',
messageId
},
'Failed creating message for %s. %s',
messageId,
err.message
);
return done(err);
});
stream.on('end', () =>
done(null, {
envelope,
messageId,
message: Buffer.concat(chunks, chunklen)
})
);
});
}
}
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 => {
const service = services[key];
const normalizedService = normalizeService(service);
normalized[normalizeKey(key)] = normalizedService;
[].concat(service.aliases || []).forEach(alias => {
normalized[normalizeKey(alias)] = normalizedService;
});
[].concat(service.domains || []).forEach(domain => {
normalized[normalizeKey(domain)] = normalizedService;
});
});
function normalizeKey(key) {
return key.replace(/[^a-zA-Z0-9.-]/g, '').toLowerCase();
}
function normalizeService(service) {
const response = {};
Object.keys(service).forEach(key => {
if (!['domains', 'aliases'].includes(key)) {
response[key] = service[key];
}
});
return response;
}
/**
* Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
* an email address (like 'test@googlemail.com').
*
* @param {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;
};

619
node_modules/nodemailer/lib/well-known/services.json generated vendored Normal file
View File

@@ -0,0 +1,619 @@
{
"1und1": {
"description": "1&1 Mail (German hosting provider)",
"host": "smtp.1und1.de",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"126": {
"description": "126 Mail (NetEase)",
"host": "smtp.126.com",
"port": 465,
"secure": true
},
"163": {
"description": "163 Mail (NetEase)",
"host": "smtp.163.com",
"port": 465,
"secure": true
},
"Aliyun": {
"description": "Alibaba Cloud Mail",
"domains": ["aliyun.com"],
"host": "smtp.aliyun.com",
"port": 465,
"secure": true
},
"AliyunQiye": {
"description": "Alibaba Cloud Enterprise Mail",
"host": "smtp.qiye.aliyun.com",
"port": 465,
"secure": true
},
"AOL": {
"description": "AOL Mail",
"domains": ["aol.com"],
"host": "smtp.aol.com",
"port": 587
},
"Aruba": {
"description": "Aruba PEC (Italian email provider)",
"domains": ["aruba.it", "pec.aruba.it"],
"aliases": ["Aruba PEC"],
"host": "smtps.aruba.it",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"Bluewin": {
"description": "Bluewin (Swiss email provider)",
"host": "smtpauths.bluewin.ch",
"domains": ["bluewin.ch"],
"port": 465
},
"BOL": {
"description": "BOL Mail (Brazilian provider)",
"domains": ["bol.com.br"],
"host": "smtp.bol.com.br",
"port": 587,
"requireTLS": true
},
"DebugMail": {
"description": "DebugMail (email testing service)",
"host": "debugmail.io",
"port": 25
},
"Disroot": {
"description": "Disroot (privacy-focused provider)",
"domains": ["disroot.org"],
"host": "disroot.org",
"port": 587,
"secure": false,
"authMethod": "LOGIN"
},
"DynectEmail": {
"description": "Dyn Email Delivery",
"aliases": ["Dynect"],
"host": "smtp.dynect.net",
"port": 25
},
"ElasticEmail": {
"description": "Elastic Email",
"aliases": ["Elastic Email"],
"host": "smtp.elasticemail.com",
"port": 465,
"secure": true
},
"Ethereal": {
"description": "Ethereal Email (email testing service)",
"aliases": ["ethereal.email"],
"host": "smtp.ethereal.email",
"port": 587
},
"FastMail": {
"description": "FastMail",
"domains": ["fastmail.fm"],
"host": "smtp.fastmail.com",
"port": 465,
"secure": true
},
"Feishu Mail": {
"description": "Feishu Mail (Lark)",
"aliases": ["Feishu", "FeishuMail"],
"domains": ["www.feishu.cn"],
"host": "smtp.feishu.cn",
"port": 465,
"secure": true
},
"Forward Email": {
"description": "Forward Email (email forwarding service)",
"aliases": ["FE", "ForwardEmail"],
"domains": ["forwardemail.net"],
"host": "smtp.forwardemail.net",
"port": 465,
"secure": true
},
"GandiMail": {
"description": "Gandi Mail",
"aliases": ["Gandi", "Gandi Mail"],
"host": "mail.gandi.net",
"port": 587
},
"Gmail": {
"description": "Gmail",
"aliases": ["Google Mail"],
"domains": ["gmail.com", "googlemail.com"],
"host": "smtp.gmail.com",
"port": 465,
"secure": true
},
"GmailWorkspace": {
"description": "Gmail Workspace",
"aliases": ["Google Workspace Mail"],
"host": "smtp-relay.gmail.com",
"port": 465,
"secure": true
},
"GMX": {
"description": "GMX Mail",
"domains": ["gmx.com", "gmx.net", "gmx.de"],
"host": "mail.gmx.com",
"port": 587
},
"Godaddy": {
"description": "GoDaddy Email (US)",
"host": "smtpout.secureserver.net",
"port": 25
},
"GodaddyAsia": {
"description": "GoDaddy Email (Asia)",
"host": "smtp.asia.secureserver.net",
"port": 25
},
"GodaddyEurope": {
"description": "GoDaddy Email (Europe)",
"host": "smtp.europe.secureserver.net",
"port": 25
},
"hot.ee": {
"description": "Hot.ee (Estonian email provider)",
"host": "mail.hot.ee"
},
"Hotmail": {
"description": "Outlook.com / Hotmail",
"aliases": ["Outlook", "Outlook.com", "Hotmail.com"],
"domains": ["hotmail.com", "outlook.com"],
"host": "smtp-mail.outlook.com",
"port": 587
},
"iCloud": {
"description": "iCloud Mail",
"aliases": ["Me", "Mac"],
"domains": ["me.com", "mac.com"],
"host": "smtp.mail.me.com",
"port": 587
},
"Infomaniak": {
"description": "Infomaniak Mail (Swiss hosting provider)",
"host": "mail.infomaniak.com",
"domains": ["ik.me", "ikmail.com", "etik.com"],
"port": 587
},
"KolabNow": {
"description": "KolabNow (secure email service)",
"domains": ["kolabnow.com"],
"aliases": ["Kolab"],
"host": "smtp.kolabnow.com",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"Loopia": {
"description": "Loopia (Swedish hosting provider)",
"host": "mailcluster.loopia.se",
"port": 465
},
"Loops": {
"description": "Loops",
"host": "smtp.loops.so",
"port": 587
},
"mail.ee": {
"description": "Mail.ee (Estonian email provider)",
"host": "smtp.mail.ee"
},
"Mail.ru": {
"description": "Mail.ru",
"host": "smtp.mail.ru",
"port": 465,
"secure": true
},
"Mailcatch.app": {
"description": "Mailcatch (email testing service)",
"host": "sandbox-smtp.mailcatch.app",
"port": 2525
},
"Maildev": {
"description": "MailDev (local email testing)",
"port": 1025,
"ignoreTLS": true
},
"MailerSend": {
"description": "MailerSend",
"host": "smtp.mailersend.net",
"port": 587
},
"Mailgun": {
"description": "Mailgun",
"host": "smtp.mailgun.org",
"port": 465,
"secure": true
},
"Mailjet": {
"description": "Mailjet",
"host": "in.mailjet.com",
"port": 587
},
"Mailosaur": {
"description": "Mailosaur (email testing service)",
"host": "mailosaur.io",
"port": 25
},
"Mailtrap": {
"description": "Mailtrap",
"host": "live.smtp.mailtrap.io",
"port": 587
},
"Mandrill": {
"description": "Mandrill (by Mailchimp)",
"host": "smtp.mandrillapp.com",
"port": 587
},
"Naver": {
"description": "Naver Mail (Korean email provider)",
"host": "smtp.naver.com",
"port": 587
},
"OhMySMTP": {
"description": "OhMySMTP (email delivery service)",
"host": "smtp.ohmysmtp.com",
"port": 587,
"secure": false
},
"One": {
"description": "One.com Email",
"host": "send.one.com",
"port": 465,
"secure": true
},
"OpenMailBox": {
"description": "OpenMailBox",
"aliases": ["OMB", "openmailbox.org"],
"host": "smtp.openmailbox.org",
"port": 465,
"secure": true
},
"Outlook365": {
"description": "Microsoft 365 / Office 365",
"host": "smtp.office365.com",
"port": 587,
"secure": false
},
"Postmark": {
"description": "Postmark",
"aliases": ["PostmarkApp"],
"host": "smtp.postmarkapp.com",
"port": 2525
},
"Proton": {
"description": "Proton Mail",
"aliases": ["ProtonMail", "Proton.me", "Protonmail.com", "Protonmail.ch"],
"domains": ["proton.me", "protonmail.com", "pm.me", "protonmail.ch"],
"host": "smtp.protonmail.ch",
"port": 587,
"requireTLS": true
},
"qiye.aliyun": {
"description": "Alibaba Mail Enterprise Edition",
"host": "smtp.mxhichina.com",
"port": "465",
"secure": true
},
"QQ": {
"description": "QQ Mail",
"domains": ["qq.com"],
"host": "smtp.qq.com",
"port": 465,
"secure": true
},
"QQex": {
"description": "QQ Enterprise Mail",
"aliases": ["QQ Enterprise"],
"domains": ["exmail.qq.com"],
"host": "smtp.exmail.qq.com",
"port": 465,
"secure": true
},
"Resend": {
"description": "Resend",
"host": "smtp.resend.com",
"port": 465,
"secure": true
},
"Runbox": {
"description": "Runbox (Norwegian email provider)",
"domains": ["runbox.com"],
"host": "smtp.runbox.com",
"port": 465,
"secure": true
},
"SendCloud": {
"description": "SendCloud (Chinese email delivery)",
"host": "smtp.sendcloud.net",
"port": 2525
},
"SendGrid": {
"description": "SendGrid",
"host": "smtp.sendgrid.net",
"port": 587
},
"SendinBlue": {
"description": "Brevo (formerly Sendinblue)",
"aliases": ["Brevo"],
"host": "smtp-relay.brevo.com",
"port": 587
},
"SendPulse": {
"description": "SendPulse",
"host": "smtp-pulse.com",
"port": 465,
"secure": true
},
"SES": {
"description": "AWS SES US East (N. Virginia)",
"host": "email-smtp.us-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-1": {
"description": "AWS SES Asia Pacific (Tokyo)",
"host": "email-smtp.ap-northeast-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-2": {
"description": "AWS SES Asia Pacific (Seoul)",
"host": "email-smtp.ap-northeast-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-3": {
"description": "AWS SES Asia Pacific (Osaka)",
"host": "email-smtp.ap-northeast-3.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTH-1": {
"description": "AWS SES Asia Pacific (Mumbai)",
"host": "email-smtp.ap-south-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTHEAST-1": {
"description": "AWS SES Asia Pacific (Singapore)",
"host": "email-smtp.ap-southeast-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTHEAST-2": {
"description": "AWS SES Asia Pacific (Sydney)",
"host": "email-smtp.ap-southeast-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-CA-CENTRAL-1": {
"description": "AWS SES Canada (Central)",
"host": "email-smtp.ca-central-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-CENTRAL-1": {
"description": "AWS SES Europe (Frankfurt)",
"host": "email-smtp.eu-central-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-NORTH-1": {
"description": "AWS SES Europe (Stockholm)",
"host": "email-smtp.eu-north-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-1": {
"description": "AWS SES Europe (Ireland)",
"host": "email-smtp.eu-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-2": {
"description": "AWS SES Europe (London)",
"host": "email-smtp.eu-west-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-3": {
"description": "AWS SES Europe (Paris)",
"host": "email-smtp.eu-west-3.amazonaws.com",
"port": 465,
"secure": true
},
"SES-SA-EAST-1": {
"description": "AWS SES South America (São Paulo)",
"host": "email-smtp.sa-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-EAST-1": {
"description": "AWS SES US East (N. Virginia)",
"host": "email-smtp.us-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-EAST-2": {
"description": "AWS SES US East (Ohio)",
"host": "email-smtp.us-east-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-GOV-EAST-1": {
"description": "AWS SES GovCloud (US-East)",
"host": "email-smtp.us-gov-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-GOV-WEST-1": {
"description": "AWS SES GovCloud (US-West)",
"host": "email-smtp.us-gov-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-WEST-1": {
"description": "AWS SES US West (N. California)",
"host": "email-smtp.us-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-WEST-2": {
"description": "AWS SES US West (Oregon)",
"host": "email-smtp.us-west-2.amazonaws.com",
"port": 465,
"secure": true
},
"Seznam": {
"description": "Seznam Email (Czech email provider)",
"aliases": ["Seznam Email"],
"domains": ["seznam.cz", "email.cz", "post.cz", "spoluzaci.cz"],
"host": "smtp.seznam.cz",
"port": 465,
"secure": true
},
"SMTP2GO": {
"description": "SMTP2GO",
"host": "mail.smtp2go.com",
"port": 2525
},
"Sparkpost": {
"description": "SparkPost",
"aliases": ["SparkPost", "SparkPost Mail"],
"domains": ["sparkpost.com"],
"host": "smtp.sparkpostmail.com",
"port": 587,
"secure": false
},
"Tipimail": {
"description": "Tipimail (email delivery service)",
"host": "smtp.tipimail.com",
"port": 587
},
"Tutanota": {
"description": "Tutanota (Tuta Mail)",
"domains": ["tutanota.com", "tuta.com", "tutanota.de", "tuta.io"],
"host": "smtp.tutanota.com",
"port": 465,
"secure": true
},
"Yahoo": {
"description": "Yahoo Mail",
"domains": ["yahoo.com"],
"host": "smtp.mail.yahoo.com",
"port": 465,
"secure": true
},
"Yandex": {
"description": "Yandex Mail",
"domains": ["yandex.ru"],
"host": "smtp.yandex.ru",
"port": 465,
"secure": true
},
"Zimbra": {
"description": "Zimbra Mail Server",
"aliases": ["Zimbra Collaboration"],
"host": "smtp.zimbra.com",
"port": 587,
"requireTLS": true
},
"Zoho": {
"description": "Zoho Mail",
"host": "smtp.zoho.com",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
}
}

436
node_modules/nodemailer/lib/xoauth2/index.js generated vendored Normal file
View File

@@ -0,0 +1,436 @@
'use strict';
const { Stream } = require('stream');
const nmfetch = require('../fetch');
const crypto = require('crypto');
const shared = require('../shared');
const errors = require('../errors');
/**
* XOAUTH2 access_token generator for Gmail.
* Create client ID for web applications in Google API console to use it.
* See Offline Access for receiving the needed refreshToken for an user
* https://developers.google.com/accounts/docs/OAuth2WebServer#offline
*
* Usage for generating access tokens with a custom method using provisionCallback:
* provisionCallback(user, renew, callback)
* * user is the username to get the token for
* * renew is a boolean that if true indicates that existing token failed and needs to be renewed
* * callback is the callback to run with (error, accessToken [, expires])
* * accessToken is a string
* * expires is an optional expire time in milliseconds
* If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself
*
* @constructor
* @param {Object} options Client information for token generation
* @param {String} options.user User e-mail address
* @param {String} options.clientId Client ID value
* @param {String} options.clientSecret Client secret value
* @param {String} options.refreshToken Refresh token for an user
* @param {String} options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
* @param {String} options.accessToken An existing valid accessToken
* @param {String} options.privateKey Private key for JSW
* @param {Number} options.expires Optional Access Token expire time in ms
* @param {Number} options.timeout Optional TTL for Access Token in seconds
* @param {Function} options.provisionCallback Function to run when a new access token is required
*/
class XOAuth2 extends Stream {
constructor(options, logger) {
super();
this.options = options || {};
if (options && options.serviceClient) {
if (!options.privateKey || !options.user) {
const err = new Error('Options "privateKey" and "user" are required for service account!');
err.code = errors.EOAUTH2;
setImmediate(() => this.emit('error', err));
return;
}
const 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 {
const timeout = Math.max(Number(this.options.timeout) || 0, 0);
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
}
this.renewing = false; // Track if renewal is in progress
this.renewalQueue = []; // Queue for pending requests during renewal
}
/**
* Returns or generates (if previous has expired) a XOAuth2 token
*
* @param {Boolean} renew If false then use cached access token (if available)
* @param {Function} callback Callback function with error object and token string
*/
getToken(renew, callback) {
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
this.logger.debug(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'reuse'
},
'Reusing existing access token for %s',
this.options.user
);
return callback(null, this.accessToken);
}
// check if it is possible to renew, if not, return the current token or error
if (!this.provisionCallback && !this.options.refreshToken && !this.options.serviceClient) {
if (this.accessToken) {
this.logger.debug(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'reuse'
},
'Reusing existing access token (no refresh capability) for %s',
this.options.user
);
return callback(null, this.accessToken);
}
this.logger.error(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'renew'
},
'Cannot renew access token for %s: No refresh mechanism available',
this.options.user
);
const err = new Error("Can't create new access token for user");
err.code = errors.EOAUTH2;
return callback(err);
}
// If renewal already in progress, queue this request instead of starting another
if (this.renewing) {
return this.renewalQueue.push({ renew, callback });
}
this.renewing = true;
// Handles token renewal completion - processes queued requests and cleans up
const generateCallback = (err, accessToken) => {
this.renewalQueue.forEach(item => item.callback(err, accessToken));
this.renewalQueue = [];
this.renewing = false;
if (err) {
this.logger.error(
{
err,
tnx: 'OAUTH2',
user: this.options.user,
action: 'renew'
},
'Failed generating new Access Token for %s',
this.options.user
);
} else {
this.logger.info(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'renew'
},
'Generated new Access Token for %s',
this.options.user
);
}
// Complete original request
callback(err, accessToken);
};
if (this.provisionCallback) {
this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => {
if (!err && accessToken) {
this.accessToken = accessToken;
this.expires = expires || 0;
}
generateCallback(err, accessToken);
});
} else {
this.generateToken(generateCallback);
}
}
/**
* Updates token values
*
* @param {String} accessToken New access token
* @param {Number} timeout Access token lifetime in seconds
*
* Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
*/
updateToken(accessToken, timeout) {
this.accessToken = accessToken;
timeout = Math.max(Number(timeout) || 0, 0);
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
this.emit('token', {
user: this.options.user,
accessToken: accessToken || '',
expires: this.expires
});
}
/**
* Generates a new XOAuth2 token with the credentials provided at initialization
*
* @param {Function} callback Callback function with error object and token string
*/
generateToken(callback) {
let urlOptions;
let loggedUrlOptions;
if (this.options.serviceClient) {
// service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
const iat = Math.floor(Date.now() / 1000); // unix time
const 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) {
const err = new Error("Can't generate token. Check your auth options");
err.code = errors.EOAUTH2;
return callback(err);
}
urlOptions = {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: token
};
loggedUrlOptions = {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: tokenData
};
} else {
if (!this.options.refreshToken) {
const err = new Error("Can't create new access token for user");
err.code = errors.EOAUTH2;
return callback(err);
}
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
urlOptions = {
client_id: this.options.clientId || '',
client_secret: this.options.clientSecret || '',
refresh_token: this.options.refreshToken,
grant_type: 'refresh_token'
};
loggedUrlOptions = {
client_id: this.options.clientId || '',
client_secret: (this.options.clientSecret || '').substr(0, 6) + '...',
refresh_token: (this.options.refreshToken || '').substr(0, 6) + '...',
grant_type: 'refresh_token'
};
}
Object.assign(urlOptions, this.options.customParams);
Object.assign(loggedUrlOptions, this.options.customParams);
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()
);
const err = new Error('Invalid authentication response');
err.code = errors.EOAUTH2;
return callback(err);
}
const logData = Object.assign({}, data);
if (logData.access_token) {
logData.access_token = (logData.access_token || '').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 + ')';
}
const err = new Error(errorMessage);
err.code = errors.EOAUTH2;
return callback(err);
}
if (data.access_token) {
this.updateToken(data.access_token, data.expires_in);
return callback(null, this.accessToken);
}
const err = new Error('No access token');
err.code = errors.EOAUTH2;
return callback(err);
});
}
/**
* Converts an access_token and user id into a base64 encoded XOAuth2 token
*
* @param {String} [accessToken] Access token string
* @return {String} Base64 encoded token for IMAP or SMTP login
*/
buildXOAuth2Token(accessToken) {
const 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;
const chunks = [];
let chunklen = 0;
const 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('.');
const signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
return payload + '.' + this.toBase64URL(signature);
}
}
module.exports = XOAuth2;

48
node_modules/nodemailer/package.json generated vendored Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "nodemailer",
"version": "8.0.4",
"description": "Easy as cake e-mail sending from your Node.js applications",
"main": "lib/nodemailer.js",
"scripts": {
"test": "node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
"test:coverage": "c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
"format": "prettier --write \"**/*.{js,json,md}\"",
"format:check": "prettier --check \"**/*.{js,json,md}\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install",
"test:syntax": "docker run --rm -v \"$PWD:/app:ro\" -w /app node:6-alpine node test/syntax-compat.js"
},
"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.1011.0",
"bunyan": "1.8.15",
"c8": "11.0.0",
"eslint": "10.0.3",
"eslint-config-prettier": "10.1.8",
"globals": "17.4.0",
"libbase64": "1.3.0",
"libmime": "5.3.7",
"libqp": "2.1.1",
"nodemailer-ntlm-auth": "1.0.4",
"prettier": "3.8.1",
"proxy": "1.0.2",
"proxy-test-server": "1.0.0",
"smtp-server": "3.18.1"
},
"engines": {
"node": ">=6.0.0"
}
}

10
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^4.21.2", "express": "^4.21.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"nodemailer": "^8.0.4",
"pg": "^8.13.1" "pg": "^8.13.1"
} }
}, },
@@ -651,6 +652,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/nodemailer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@@ -10,6 +10,7 @@
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^4.21.2", "express": "^4.21.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"nodemailer": "^8.0.4",
"pg": "^8.13.1" "pg": "^8.13.1"
} }
} }

View File

@@ -10,7 +10,8 @@
<!-- Auth Modal ──────────────────────────────────────────────────────────── --> <!-- Auth Modal ──────────────────────────────────────────────────────────── -->
<div class="authOverlay" id="authOverlay"> <div class="authOverlay" id="authOverlay">
<div class="authModal"> <div class="authModal">
<div class="authTabs"> <!-- Normal tabs (login / register) -->
<div class="authTabs" id="authTabsBar">
<button type="button" class="authTab authTab--active" id="tabLogin">Se connecter</button> <button type="button" class="authTab authTab--active" id="tabLogin">Se connecter</button>
<button type="button" class="authTab" id="tabRegister">S'enregistrer</button> <button type="button" class="authTab" id="tabRegister">S'enregistrer</button>
</div> </div>
@@ -27,6 +28,7 @@
</div> </div>
<div class="authError hidden" id="loginError"></div> <div class="authError hidden" id="loginError"></div>
<button type="submit" class="authSubmit">Se connecter</button> <button type="submit" class="authSubmit">Se connecter</button>
<button type="button" class="authLink" id="forgotPasswordBtn">Mot de passe oublié ?</button>
</form> </form>
<!-- Register form --> <!-- Register form -->
@@ -59,6 +61,51 @@
<div class="authError hidden" id="registerError"></div> <div class="authError hidden" id="registerError"></div>
<button type="submit" class="authSubmit">Créer le compte</button> <button type="submit" class="authSubmit">Créer le compte</button>
</form> </form>
<!-- Forgot password form -->
<div class="authPanel hidden" id="forgotPanel">
<p class="authPanelTitle">Mot de passe oublié</p>
<p class="authPanelDesc">Entrez votre adresse courriel pour recevoir un lien de réinitialisation.</p>
<form id="forgotForm">
<div class="authField">
<label>Adresse courriel</label>
<input type="email" id="forgotEmail" autocomplete="email" required />
</div>
<div class="authError hidden" id="forgotError"></div>
<button type="submit" class="authSubmit">Envoyer le lien</button>
</form>
<button type="button" class="authLink" id="backToLoginBtn">← Retour à la connexion</button>
</div>
<!-- Check your email message (after register or forgot password) -->
<div class="authPanel hidden" id="checkEmailPanel">
<div class="authSuccessIcon"></div>
<p class="authPanelTitle" id="checkEmailTitle">Vérifiez votre courriel</p>
<p class="authPanelDesc" id="checkEmailMsg">Un email de confirmation a été envoyé. Cliquez sur le lien dans l'email pour activer votre compte.</p>
<button type="button" class="authSubmit authSubmit--ghost" id="resendEmailBtn">Renvoyer l'email</button>
<button type="button" class="authLink" id="backToLoginBtn2">← Retour à la connexion</button>
</div>
<!-- Reset password form (shown when ?reset=TOKEN is in URL) -->
<div class="authPanel hidden" id="resetPanel">
<p class="authPanelTitle">Nouveau mot de passe</p>
<form id="resetForm">
<div class="authField">
<label>Nouveau mot de passe <span class="authHint">(6 caractères min.)</span></label>
<input type="password" id="resetPassword" autocomplete="new-password" required />
</div>
<div class="authError hidden" id="resetError"></div>
<button type="submit" class="authSubmit">Réinitialiser</button>
</form>
</div>
<!-- Email confirmed success (shown after redirect) -->
<div class="authPanel hidden" id="confirmedPanel">
<div class="authSuccessIcon"></div>
<p class="authPanelTitle">Email confirmé !</p>
<p class="authPanelDesc">Votre adresse email a été confirmée. Vous pouvez maintenant vous connecter.</p>
<button type="button" class="authSubmit" id="goToLoginBtn">Se connecter</button>
</div>
</div> </div>
</div> </div>

View File

@@ -50,6 +50,30 @@ export async function apiRegister(username, email, password, team) {
}); });
} }
export async function apiResendConfirmation(email) {
return fetch("/api/auth/resend-confirmation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
}
export async function apiForgotPassword(email) {
return fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
}
export async function apiResetPassword(token, password) {
return fetch("/api/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, password }),
});
}
export async function apiGetMe(token) { export async function apiGetMe(token) {
return fetch("/api/auth/me", { return fetch("/api/auth/me", {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },

View File

@@ -1,13 +1,18 @@
import { apiLogin, apiRegister, apiGetMe } from "./api.js"; import { apiLogin, apiRegister, apiGetMe, apiResendConfirmation, apiForgotPassword, apiResetPassword } from "./api.js";
import { setCurrentTeam, refreshFromServer } from "./game.js"; import { setCurrentTeam, refreshFromServer } from "./game.js";
// ── DOM refs ────────────────────────────────────────────────────────────────── // ── DOM refs ──────────────────────────────────────────────────────────────────
const authOverlay = document.getElementById("authOverlay"); const authOverlay = document.getElementById("authOverlay");
const authTabsBar = document.getElementById("authTabsBar");
const tabLogin = document.getElementById("tabLogin"); const tabLogin = document.getElementById("tabLogin");
const tabRegister = document.getElementById("tabRegister"); const tabRegister = document.getElementById("tabRegister");
const loginForm = document.getElementById("loginForm"); const loginForm = document.getElementById("loginForm");
const registerForm = document.getElementById("registerForm"); const registerForm = document.getElementById("registerForm");
const forgotPanel = document.getElementById("forgotPanel");
const checkEmailPanel = document.getElementById("checkEmailPanel");
const resetPanel = document.getElementById("resetPanel");
const confirmedPanel = document.getElementById("confirmedPanel");
const loginUsernameEl = document.getElementById("loginUsername"); const loginUsernameEl = document.getElementById("loginUsername");
const loginPasswordEl = document.getElementById("loginPassword"); const loginPasswordEl = document.getElementById("loginPassword");
const loginErrorEl = document.getElementById("loginError"); const loginErrorEl = document.getElementById("loginError");
@@ -15,6 +20,14 @@ const regUsernameEl = document.getElementById("regUsername");
const regEmailEl = document.getElementById("regEmail"); const regEmailEl = document.getElementById("regEmail");
const regPasswordEl = document.getElementById("regPassword"); const regPasswordEl = document.getElementById("regPassword");
const registerErrorEl = document.getElementById("registerError"); const registerErrorEl = document.getElementById("registerError");
const forgotForm = document.getElementById("forgotForm");
const forgotEmailEl = document.getElementById("forgotEmail");
const forgotErrorEl = document.getElementById("forgotError");
const resetForm = document.getElementById("resetForm");
const resetPasswordEl = document.getElementById("resetPassword");
const resetErrorEl = document.getElementById("resetError");
const checkEmailMsg = document.getElementById("checkEmailMsg");
const resendEmailBtn = document.getElementById("resendEmailBtn");
const userDisplayEl = document.getElementById("userDisplay"); const userDisplayEl = document.getElementById("userDisplay");
const logoutBtn = document.getElementById("logoutBtn"); const logoutBtn = document.getElementById("logoutBtn");
@@ -23,11 +36,39 @@ const logoutBtn = document.getElementById("logoutBtn");
export let authToken = localStorage.getItem("authToken") ?? null; export let authToken = localStorage.getItem("authToken") ?? null;
export let currentUser = null; export let currentUser = null;
// Tracks which email to use for resend (set after register)
let pendingEmail = null;
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
function showError(el, msg) { el.textContent = msg; el.classList.remove("hidden"); } function showError(el, msg) { el.textContent = msg; el.classList.remove("hidden"); }
function clearError(el) { el.textContent = ""; el.classList.add("hidden"); } function clearError(el) { el.textContent = ""; el.classList.add("hidden"); }
// Show only one panel, hide all others
const allPanels = [loginForm, registerForm, forgotPanel, checkEmailPanel, resetPanel, confirmedPanel];
function showPanel(panel) {
for (const p of allPanels) p.classList.add("hidden");
authTabsBar.classList.add("hidden");
panel.classList.remove("hidden");
}
function showLoginTab() {
for (const p of allPanels) p.classList.add("hidden");
authTabsBar.classList.remove("hidden");
loginForm.classList.remove("hidden");
tabLogin.classList.add("authTab--active");
tabRegister.classList.remove("authTab--active");
}
function showRegisterTab() {
for (const p of allPanels) p.classList.add("hidden");
authTabsBar.classList.remove("hidden");
registerForm.classList.remove("hidden");
tabRegister.classList.add("authTab--active");
tabLogin.classList.remove("authTab--active");
}
export function showAuthOverlay() { authOverlay.classList.remove("hidden"); } export function showAuthOverlay() { authOverlay.classList.remove("hidden"); }
export function hideAuthOverlay() { authOverlay.classList.add("hidden"); } export function hideAuthOverlay() { authOverlay.classList.add("hidden"); }
@@ -49,6 +90,7 @@ function logout() {
userDisplayEl.textContent = "—"; userDisplayEl.textContent = "—";
logoutBtn.classList.add("hidden"); logoutBtn.classList.add("hidden");
showAuthOverlay(); showAuthOverlay();
showLoginTab();
} }
// ── Session restore ─────────────────────────────────────────────────────────── // ── Session restore ───────────────────────────────────────────────────────────
@@ -66,21 +108,63 @@ export async function tryRestoreSession() {
} }
} }
// ── Handle URL params (email confirmation / password reset) ───────────────────
export function handleUrlEmailFlows() {
const params = new URLSearchParams(window.location.search);
if (params.has("email_confirm")) {
// Trigger confirmation via API redirect — this is handled server-side.
// The server redirects to /?email_confirmed=1 or /?confirm_error=...
const token = params.get("email_confirm");
window.history.replaceState({}, "", "/");
// Call server-side confirm endpoint by navigating (GET request)
window.location.href = `/api/auth/confirm-email?token=${encodeURIComponent(token)}`;
return true;
}
if (params.has("email_confirmed")) {
window.history.replaceState({}, "", "/");
showAuthOverlay();
showPanel(confirmedPanel);
return true;
}
if (params.has("confirm_error")) {
const err = params.get("confirm_error");
window.history.replaceState({}, "", "/");
showAuthOverlay();
showLoginTab();
const msgs = {
expired: "Le lien de confirmation a expiré. Connectez-vous pour en recevoir un nouveau.",
invalid: "Lien de confirmation invalide.",
server_error: "Erreur serveur. Réessayez plus tard.",
};
showError(loginErrorEl, msgs[err] ?? "Erreur de confirmation.");
return true;
}
if (params.has("reset")) {
const token = params.get("reset");
window.history.replaceState({}, "", "/");
showAuthOverlay();
resetPanel.dataset.token = token;
showPanel(resetPanel);
return true;
}
return false;
}
// ── Tab switching ───────────────────────────────────────────────────────────── // ── Tab switching ─────────────────────────────────────────────────────────────
tabLogin.addEventListener("click", () => { tabLogin.addEventListener("click", () => {
tabLogin.classList.add("authTab--active"); showLoginTab();
tabRegister.classList.remove("authTab--active");
loginForm.classList.remove("hidden");
registerForm.classList.add("hidden");
clearError(loginErrorEl); clearError(loginErrorEl);
}); });
tabRegister.addEventListener("click", () => { tabRegister.addEventListener("click", () => {
tabRegister.classList.add("authTab--active"); showRegisterTab();
tabLogin.classList.remove("authTab--active");
registerForm.classList.remove("hidden");
loginForm.classList.add("hidden");
clearError(registerErrorEl); clearError(registerErrorEl);
}); });
@@ -96,15 +180,23 @@ loginForm.addEventListener("submit", async (e) => {
const res = await apiLogin(username, password); const res = await apiLogin(username, password);
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
const msgs = { invalid_credentials: "Invalid username or password.", missing_fields: "Please fill in all fields." }; if (data.error === "email_not_verified") {
showError(loginErrorEl, msgs[data.error] ?? "Login failed."); // Show check-email panel with resend option
pendingEmail = null; // email unknown here, ask user to re-register or resend
checkEmailMsg.textContent = "Votre adresse email n'a pas encore été confirmée. Vérifiez votre boîte de réception ou renvoyez l'email de confirmation.";
resendEmailBtn.dataset.email = "";
showPanel(checkEmailPanel);
return;
}
const msgs = { invalid_credentials: "Identifiant ou mot de passe invalide.", missing_fields: "Veuillez remplir tous les champs." };
showError(loginErrorEl, msgs[data.error] ?? "Connexion échouée.");
return; return;
} }
applyUser(data.user, data.token); applyUser(data.user, data.token);
hideAuthOverlay(); hideAuthOverlay();
await refreshFromServer(); await refreshFromServer();
} catch { } catch {
showError(loginErrorEl, "Network error. Try again."); showError(loginErrorEl, "Erreur réseau. Réessayez.");
} }
}); });
@@ -117,30 +209,134 @@ registerForm.addEventListener("submit", async (e) => {
const email = regEmailEl.value.trim(); const email = regEmailEl.value.trim();
const password = regPasswordEl.value; const password = regPasswordEl.value;
const teamInput = registerForm.querySelector('input[name="regTeam"]:checked'); const teamInput = registerForm.querySelector('input[name="regTeam"]:checked');
if (!teamInput) { showError(registerErrorEl, "Please choose a team."); return; } if (!teamInput) { showError(registerErrorEl, "Veuillez choisir une équipe."); return; }
try { try {
const res = await apiRegister(username, email, password, teamInput.value); const res = await apiRegister(username, email, password, teamInput.value);
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
const msgs = { const msgs = {
username_taken: "This username is already taken.", username_taken: "Ce nom d'utilisateur est déjà pris.",
email_taken: "This email is already registered.", email_taken: "Cette adresse email est déjà utilisée.",
password_too_short:"Password must be at least 6 characters.", password_too_short:"Le mot de passe doit comporter au moins 6 caractères.",
invalid_username: "Username must be 232 characters.", invalid_username: "Le nom d'utilisateur doit comporter entre 2 et 32 caractères.",
missing_fields: "Please fill in all fields.", missing_fields: "Veuillez remplir tous les champs.",
invalid_team: "Invalid team selected.", invalid_team: "Équipe invalide.",
}; };
showError(registerErrorEl, msgs[data.error] ?? "Registration failed."); showError(registerErrorEl, msgs[data.error] ?? "Inscription échouée.");
return; return;
} }
applyUser(data.user, data.token); // Registration successful — pending email confirmation
hideAuthOverlay(); pendingEmail = email;
await refreshFromServer(); resendEmailBtn.dataset.email = email;
checkEmailMsg.textContent = `Un email de confirmation a été envoyé à ${email}. Cliquez sur le lien pour activer votre compte.`;
document.getElementById("checkEmailTitle").textContent = "Vérifiez votre courriel";
showPanel(checkEmailPanel);
} catch { } catch {
showError(registerErrorEl, "Network error. Try again."); showError(registerErrorEl, "Erreur réseau. Réessayez.");
} }
}); });
// ── Forgot password ───────────────────────────────────────────────────────────
document.getElementById("forgotPasswordBtn").addEventListener("click", () => {
clearError(forgotErrorEl);
forgotEmailEl.value = "";
showPanel(forgotPanel);
});
document.getElementById("backToLoginBtn").addEventListener("click", () => {
showLoginTab();
clearError(loginErrorEl);
});
forgotForm.addEventListener("submit", async (e) => {
e.preventDefault();
clearError(forgotErrorEl);
const email = forgotEmailEl.value.trim();
if (!email) return;
try {
await apiForgotPassword(email);
// Always show the same message regardless of whether email exists (anti-enumeration)
checkEmailMsg.textContent = `Si un compte existe pour ${email}, un lien de réinitialisation a été envoyé.`;
document.getElementById("checkEmailTitle").textContent = "Email envoyé";
resendEmailBtn.dataset.email = "";
resendEmailBtn.classList.add("hidden");
showPanel(checkEmailPanel);
} catch {
showError(forgotErrorEl, "Erreur réseau. Réessayez.");
}
});
// ── Check-email panel ─────────────────────────────────────────────────────────
document.getElementById("backToLoginBtn2").addEventListener("click", () => {
resendEmailBtn.classList.remove("hidden");
showLoginTab();
clearError(loginErrorEl);
});
resendEmailBtn.addEventListener("click", async () => {
const email = resendEmailBtn.dataset.email;
if (!email) return;
resendEmailBtn.disabled = true;
resendEmailBtn.textContent = "Envoi…";
try {
await apiResendConfirmation(email);
resendEmailBtn.textContent = "Email renvoyé !";
} catch {
resendEmailBtn.textContent = "Erreur — réessayez";
} finally {
setTimeout(() => {
resendEmailBtn.disabled = false;
resendEmailBtn.textContent = "Renvoyer l'email";
}, 3000);
}
});
// ── Reset password form ───────────────────────────────────────────────────────
resetForm.addEventListener("submit", async (e) => {
e.preventDefault();
clearError(resetErrorEl);
const token = resetPanel.dataset.token;
const password = resetPasswordEl.value;
if (!token) { showError(resetErrorEl, "Token manquant."); return; }
try {
const res = await apiResetPassword(token, password);
const data = await res.json();
if (!res.ok) {
const msgs = {
invalid_token: "Ce lien est invalide.",
token_expired: "Ce lien a expiré. Refaites une demande.",
password_too_short: "Le mot de passe doit comporter au moins 6 caractères.",
missing_fields: "Veuillez remplir tous les champs.",
};
showError(resetErrorEl, msgs[data.error] ?? "Erreur lors de la réinitialisation.");
return;
}
showLoginTab();
showError(loginErrorEl, ""); // clear
clearError(loginErrorEl);
// Show success in login form briefly
loginErrorEl.style.color = "rgba(100,220,100,0.95)";
loginErrorEl.style.background = "rgba(30,100,30,0.12)";
loginErrorEl.style.border = "1px solid rgba(30,100,30,0.25)";
showError(loginErrorEl, "Mot de passe réinitialisé ! Vous pouvez vous connecter.");
setTimeout(() => {
loginErrorEl.removeAttribute("style");
clearError(loginErrorEl);
}, 6000);
} catch {
showError(resetErrorEl, "Erreur réseau. Réessayez.");
}
});
// ── Confirmed panel ───────────────────────────────────────────────────────────
document.getElementById("goToLoginBtn").addEventListener("click", () => {
showLoginTab();
});
// ── Logout ──────────────────────────────────────────────────────────────────── // ── Logout ────────────────────────────────────────────────────────────────────
logoutBtn.addEventListener("click", logout); logoutBtn.addEventListener("click", logout);

View File

@@ -20,6 +20,7 @@ import {
tryRestoreSession, tryRestoreSession,
showAuthOverlay, showAuthOverlay,
hideAuthOverlay, hideAuthOverlay,
handleUrlEmailFlows,
} from "./auth.js"; } from "./auth.js";
// ── DOM refs ────────────────────────────────────────────────────────────────── // ── DOM refs ──────────────────────────────────────────────────────────────────
@@ -90,11 +91,15 @@ async function boot() {
// Load the SVG playfield mask before any drawing or data fetch // Load the SVG playfield mask before any drawing or data fetch
await loadPlayfieldMask(); await loadPlayfieldMask();
const restored = await tryRestoreSession(); // Handle email confirmation / password reset URL params first
if (!restored) { const urlHandled = handleUrlEmailFlows();
showAuthOverlay(); if (!urlHandled) {
} else { const restored = await tryRestoreSession();
hideAuthOverlay(); if (!restored) {
showAuthOverlay();
} else {
hideAuthOverlay();
}
} }
try { try {

View File

@@ -192,6 +192,76 @@ body {
background: rgba(113, 199, 255, 0.28); background: rgba(113, 199, 255, 0.28);
} }
.authSubmit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.authSubmit--ghost {
background: transparent;
border-color: rgba(255, 255, 255, 0.15);
}
.authSubmit--ghost:hover {
background: rgba(255, 255, 255, 0.06);
}
/* Link-style button inside auth modal */
.authLink {
margin-top: 2px;
background: none;
border: none;
color: rgba(113, 199, 255, 0.75);
font-size: 13px;
cursor: pointer;
text-align: center;
padding: 4px 0;
transition: color 0.15s;
}
.authLink:hover {
color: rgba(113, 199, 255, 1);
}
/* Free-standing panel (not a form) inside the auth modal */
.authPanel {
padding: 28px 22px 24px;
display: flex;
flex-direction: column;
gap: 14px;
text-align: center;
}
.authPanel.hidden {
display: none;
}
.authPanel form {
display: flex;
flex-direction: column;
gap: 14px;
text-align: left;
}
.authPanelTitle {
font-size: 17px;
font-weight: 700;
color: #e9eef6;
margin: 0;
}
.authPanelDesc {
font-size: 13px;
color: rgba(233, 238, 246, 0.65);
margin: 0;
line-height: 1.5;
}
.authSuccessIcon {
font-size: 40px;
line-height: 1;
opacity: 0.85;
}
/* ── Score board ──────────────────────────────────────────────────────────── */ /* ── Score board ──────────────────────────────────────────────────────────── */

View File

@@ -11,6 +11,23 @@ export async function initUsersSchema() {
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
team TEXT NOT NULL CHECK (team IN ('blue', 'red')), team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')), role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
email_verified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`);
// Add email_verified to existing deployments that lack it
await usersPool.query(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT false;
`);
await usersPool.query(`
CREATE TABLE IF NOT EXISTS email_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
type TEXT NOT NULL CHECK (type IN ('confirm', 'reset')),
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
`); `);
@@ -22,7 +39,7 @@ export async function createUser(username, email, passwordHash, team) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`INSERT INTO users (username, email, password_hash, team) `INSERT INTO users (username, email, password_hash, team)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, username, email, team, role`, RETURNING id, username, email, team, role, email_verified`,
[username, email, passwordHash, team] [username, email, passwordHash, team]
); );
return rows[0]; return rows[0];
@@ -30,16 +47,64 @@ export async function createUser(username, email, passwordHash, team) {
export async function getUserByUsername(username) { export async function getUserByUsername(username) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`SELECT id, username, email, team, role, password_hash FROM users WHERE username = $1`, `SELECT id, username, email, team, role, password_hash, email_verified FROM users WHERE username = $1`,
[username] [username]
); );
return rows[0] ?? null; return rows[0] ?? null;
} }
export async function getUserByEmail(email) {
const { rows } = await usersPool.query(
`SELECT id, username, email, team, role, email_verified FROM users WHERE email = $1`,
[email]
);
return rows[0] ?? null;
}
export async function getUserById(id) { export async function getUserById(id) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`SELECT id, username, email, team, role FROM users WHERE id = $1`, `SELECT id, username, email, team, role, email_verified FROM users WHERE id = $1`,
[id] [id]
); );
return rows[0] ?? null; return rows[0] ?? null;
} }
export async function createEmailToken(userId, token, type, expiresAt) {
// Delete any existing token of the same type for this user
await usersPool.query(
`DELETE FROM email_tokens WHERE user_id = $1 AND type = $2`,
[userId, type]
);
await usersPool.query(
`INSERT INTO email_tokens (user_id, token, type, expires_at) VALUES ($1, $2, $3, $4)`,
[userId, token, type, expiresAt]
);
}
export async function getEmailToken(token, type) {
const { rows } = await usersPool.query(
`SELECT et.*, u.email FROM email_tokens et
JOIN users u ON u.id = et.user_id
WHERE et.token = $1 AND et.type = $2`,
[token, type]
);
return rows[0] ?? null;
}
export async function deleteEmailToken(token) {
await usersPool.query(`DELETE FROM email_tokens WHERE token = $1`, [token]);
}
export async function markEmailVerified(userId) {
await usersPool.query(
`UPDATE users SET email_verified = true WHERE id = $1`,
[userId]
);
}
export async function updatePassword(userId, passwordHash) {
await usersPool.query(
`UPDATE users SET password_hash = $1 WHERE id = $2`,
[passwordHash, userId]
);
}

45
server/email.js Normal file
View File

@@ -0,0 +1,45 @@
import nodemailer from "nodemailer";
const transport = nodemailer.createTransport({
host: process.env.SMTP_HOST ?? "mailpit",
port: Number(process.env.SMTP_PORT ?? 1025),
secure: process.env.SMTP_SECURE === "true",
...(process.env.SMTP_USER
? { auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }
: {}),
});
const FROM = process.env.SMTP_FROM ?? "Star Wars Wild Space <noreply@wildspace.local>";
const APP_URL = (process.env.APP_URL ?? "http://localhost:8080").replace(/\/$/, "");
export async function sendConfirmationEmail(to, token) {
const url = `${APP_URL}/?email_confirm=${encodeURIComponent(token)}`;
await transport.sendMail({
from: FROM,
to,
subject: "Confirmez votre adresse email — Star Wars Wild Space",
text: `Cliquez sur le lien ci-dessous pour confirmer votre compte :\n\n${url}\n\nCe lien expire dans 24 heures.`,
html: `
<h2>Star Wars — Wild Space</h2>
<p>Cliquez sur le lien ci-dessous pour confirmer votre adresse email :</p>
<p><a href="${url}" style="font-size:16px">${url}</a></p>
<p><small>Ce lien expire dans 24 heures.</small></p>
`,
});
}
export async function sendPasswordResetEmail(to, token) {
const url = `${APP_URL}/?reset=${encodeURIComponent(token)}`;
await transport.sendMail({
from: FROM,
to,
subject: "Réinitialisation de mot de passe — Star Wars Wild Space",
text: `Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe :\n\n${url}\n\nCe lien expire dans 1 heure. Si vous n'avez pas fait cette demande, ignorez cet email.`,
html: `
<h2>Star Wars — Wild Space</h2>
<p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe :</p>
<p><a href="${url}" style="font-size:16px">${url}</a></p>
<p><small>Ce lien expire dans 1 heure. Si vous n'avez pas fait cette demande, vous pouvez ignorer cet email.</small></p>
`,
});
}

View File

@@ -1,8 +1,20 @@
import express from "express"; import express from "express";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import crypto from "crypto";
import { JWT_SECRET, authMiddleware } from "../middleware/auth.js"; import { JWT_SECRET, authMiddleware } from "../middleware/auth.js";
import { createUser, getUserByUsername, getUserById } from "../db/usersDb.js"; import {
createUser,
getUserByUsername,
getUserByEmail,
getUserById,
createEmailToken,
getEmailToken,
deleteEmailToken,
markEmailVerified,
updatePassword,
} from "../db/usersDb.js";
import { sendConfirmationEmail, sendPasswordResetEmail } from "../email.js";
const router = express.Router(); const router = express.Router();
@@ -14,6 +26,10 @@ function issueToken(user) {
); );
} }
function generateToken() {
return crypto.randomBytes(32).toString("hex");
}
// POST /api/auth/register // POST /api/auth/register
router.post("/register", async (req, res) => { router.post("/register", async (req, res) => {
const { username, email, password, team } = req.body ?? {}; const { username, email, password, team } = req.body ?? {};
@@ -32,11 +48,16 @@ router.post("/register", async (req, res) => {
try { try {
const passwordHash = await bcrypt.hash(password, 12); const passwordHash = await bcrypt.hash(password, 12);
const user = await createUser(username.trim(), email.trim().toLowerCase(), passwordHash, team); const user = await createUser(username.trim(), email.trim().toLowerCase(), passwordHash, team);
const token = issueToken(user);
return res.status(201).json({ // Create and send confirmation token
token, const token = generateToken();
user: { id: user.id, username: user.username, team: user.team, role: user.role }, const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
}); await createEmailToken(user.id, token, "confirm", expiresAt);
sendConfirmationEmail(user.email, token).catch((e) =>
console.error("[email] Failed to send confirmation email:", e.message)
);
return res.status(201).json({ pending_verification: true });
} catch (e) { } catch (e) {
if (e.code === "23505") { if (e.code === "23505") {
if (e.constraint?.includes("email")) return res.status(409).json({ error: "email_taken" }); if (e.constraint?.includes("email")) return res.status(409).json({ error: "email_taken" });
@@ -56,6 +77,7 @@ router.post("/login", async (req, res) => {
if (!user) return res.status(401).json({ error: "invalid_credentials" }); if (!user) return res.status(401).json({ error: "invalid_credentials" });
const valid = await bcrypt.compare(password, user.password_hash); const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) return res.status(401).json({ error: "invalid_credentials" }); if (!valid) return res.status(401).json({ error: "invalid_credentials" });
if (!user.email_verified) return res.status(403).json({ error: "email_not_verified" });
const token = issueToken(user); const token = issueToken(user);
return res.json({ return res.json({
token, token,
@@ -80,4 +102,92 @@ router.get("/me", authMiddleware, async (req, res) => {
} }
}); });
// POST /api/auth/resend-confirmation
router.post("/resend-confirmation", async (req, res) => {
const { email } = req.body ?? {};
if (!email) return res.status(400).json({ error: "missing_fields" });
try {
const user = await getUserByEmail(email.trim().toLowerCase());
// Always respond OK to avoid user enumeration
if (!user || user.email_verified) return res.json({ ok: true });
const token = generateToken();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
await createEmailToken(user.id, token, "confirm", expiresAt);
sendConfirmationEmail(user.email, token).catch((e) =>
console.error("[email] Failed to resend confirmation email:", e.message)
);
return res.json({ ok: true });
} catch (e) {
console.error(e);
return res.status(500).json({ error: "database_error" });
}
});
// GET /api/auth/confirm-email?token=TOKEN
router.get("/confirm-email", async (req, res) => {
const { token } = req.query;
if (!token) return res.redirect("/?confirm_error=invalid");
try {
const row = await getEmailToken(token, "confirm");
if (!row) return res.redirect("/?confirm_error=invalid");
if (new Date(row.expires_at) < new Date()) {
await deleteEmailToken(token);
return res.redirect("/?confirm_error=expired");
}
await markEmailVerified(row.user_id);
await deleteEmailToken(token);
return res.redirect("/?email_confirmed=1");
} catch (e) {
console.error(e);
return res.redirect("/?confirm_error=server_error");
}
});
// POST /api/auth/forgot-password
router.post("/forgot-password", async (req, res) => {
const { email } = req.body ?? {};
if (!email) return res.status(400).json({ error: "missing_fields" });
try {
const user = await getUserByEmail(email.trim().toLowerCase());
// Always respond OK to avoid user enumeration
if (!user) return res.json({ ok: true });
const token = generateToken();
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await createEmailToken(user.id, token, "reset", expiresAt);
sendPasswordResetEmail(user.email, token).catch((e) =>
console.error("[email] Failed to send reset email:", e.message)
);
return res.json({ ok: true });
} catch (e) {
console.error(e);
return res.status(500).json({ error: "database_error" });
}
});
// POST /api/auth/reset-password
router.post("/reset-password", async (req, res) => {
const { token, password } = req.body ?? {};
if (!token || !password) return res.status(400).json({ error: "missing_fields" });
if (typeof password !== "string" || password.length < 6) {
return res.status(400).json({ error: "password_too_short" });
}
try {
const row = await getEmailToken(token, "reset");
if (!row) return res.status(400).json({ error: "invalid_token" });
if (new Date(row.expires_at) < new Date()) {
await deleteEmailToken(token);
return res.status(400).json({ error: "token_expired" });
}
const passwordHash = await bcrypt.hash(password, 12);
await updatePassword(row.user_id, passwordHash);
await deleteEmailToken(token);
return res.json({ ok: true });
} catch (e) {
console.error(e);
return res.status(500).json({ error: "database_error" });
}
});
export default router; export default router;