diff --git a/backend/controllers/myTischtennisController.js b/backend/controllers/myTischtennisController.js index 9c8d92c..7afe872 100644 --- a/backend/controllers/myTischtennisController.js +++ b/backend/controllers/myTischtennisController.js @@ -42,7 +42,7 @@ class MyTischtennisController { async upsertAccount(req, res, next) { try { const userId = req.user.id; - const { email, password, savePassword, userPassword } = req.body; + const { email, password, savePassword, autoUpdateRatings, userPassword } = req.body; if (!email) { throw new HttpError(400, 'E-Mail-Adresse erforderlich'); @@ -58,6 +58,7 @@ class MyTischtennisController { email, password, savePassword || false, + autoUpdateRatings || false, userPassword ); @@ -127,6 +128,20 @@ class MyTischtennisController { next(error); } } + + /** + * GET /api/mytischtennis/update-history + * Get update ratings history + */ + async getUpdateHistory(req, res, next) { + try { + const userId = req.user.id; + const history = await myTischtennisService.getUpdateHistory(userId); + res.status(200).json({ history }); + } catch (error) { + next(error); + } + } } export default new MyTischtennisController(); diff --git a/backend/migrations/add_auto_update_ratings_to_my_tischtennis.sql b/backend/migrations/add_auto_update_ratings_to_my_tischtennis.sql new file mode 100644 index 0000000..fff58b3 --- /dev/null +++ b/backend/migrations/add_auto_update_ratings_to_my_tischtennis.sql @@ -0,0 +1,13 @@ +-- Migration: Add auto update ratings fields to my_tischtennis table +-- Date: 2025-01-27 + +-- Add auto_update_ratings column +ALTER TABLE my_tischtennis +ADD COLUMN auto_update_ratings BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add last_update_ratings column +ALTER TABLE my_tischtennis +ADD COLUMN last_update_ratings TIMESTAMP NULL; + +-- Create index for auto_update_ratings for efficient querying +CREATE INDEX idx_my_tischtennis_auto_update_ratings ON my_tischtennis(auto_update_ratings); diff --git a/backend/migrations/create_my_tischtennis_update_history.sql b/backend/migrations/create_my_tischtennis_update_history.sql new file mode 100644 index 0000000..72f652d --- /dev/null +++ b/backend/migrations/create_my_tischtennis_update_history.sql @@ -0,0 +1,23 @@ +-- Migration: Create my_tischtennis_update_history table +-- Date: 2025-01-27 +-- For MariaDB + +CREATE TABLE IF NOT EXISTS my_tischtennis_update_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + success BOOLEAN NOT NULL DEFAULT FALSE, + message TEXT, + error_details TEXT, + updated_count INT DEFAULT 0, + execution_time INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + CONSTRAINT fk_my_tischtennis_update_history_user_id + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Create indexes for efficient querying +CREATE INDEX idx_my_tischtennis_update_history_user_id ON my_tischtennis_update_history(user_id); +CREATE INDEX idx_my_tischtennis_update_history_created_at ON my_tischtennis_update_history(created_at); +CREATE INDEX idx_my_tischtennis_update_history_success ON my_tischtennis_update_history(success); diff --git a/backend/models/MyTischtennis.js b/backend/models/MyTischtennis.js index 5517249..433a325 100644 --- a/backend/models/MyTischtennis.js +++ b/backend/models/MyTischtennis.js @@ -34,6 +34,12 @@ const MyTischtennis = sequelize.define('MyTischtennis', { allowNull: false, field: 'save_password' }, + autoUpdateRatings: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + field: 'auto_update_ratings' + }, accessToken: { type: DataTypes.TEXT, allowNull: true, @@ -82,6 +88,11 @@ const MyTischtennis = sequelize.define('MyTischtennis', { type: DataTypes.DATE, allowNull: true, field: 'last_login_success' + }, + lastUpdateRatings: { + type: DataTypes.DATE, + allowNull: true, + field: 'last_update_ratings' } }, { underscored: true, diff --git a/backend/models/MyTischtennisUpdateHistory.js b/backend/models/MyTischtennisUpdateHistory.js new file mode 100644 index 0000000..780b168 --- /dev/null +++ b/backend/models/MyTischtennisUpdateHistory.js @@ -0,0 +1,63 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; + +const MyTischtennisUpdateHistory = sequelize.define('MyTischtennisUpdateHistory', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'user', + key: 'id' + }, + onDelete: 'CASCADE' + }, + success: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + message: { + type: DataTypes.TEXT, + allowNull: true + }, + errorDetails: { + type: DataTypes.TEXT, + allowNull: true, + field: 'error_details' + }, + updatedCount: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 0, + field: 'updated_count' + }, + executionTime: { + type: DataTypes.INTEGER, + allowNull: true, + comment: 'Execution time in milliseconds', + field: 'execution_time' + } +}, { + underscored: true, + tableName: 'my_tischtennis_update_history', + timestamps: true, + indexes: [ + { + fields: ['user_id'] + }, + { + fields: ['created_at'] + }, + { + fields: ['success'] + } + ] +}); + +export default MyTischtennisUpdateHistory; diff --git a/backend/models/index.js b/backend/models/index.js index 73e14f7..86ec207 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -36,6 +36,7 @@ import OfficialTournament from './OfficialTournament.js'; import OfficialCompetition from './OfficialCompetition.js'; import OfficialCompetitionMember from './OfficialCompetitionMember.js'; import MyTischtennis from './MyTischtennis.js'; +import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js'; // Official tournaments relations OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' }); OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' }); @@ -227,6 +228,9 @@ DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' }); User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' }); MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' }); +User.hasMany(MyTischtennisUpdateHistory, { foreignKey: 'userId', as: 'updateHistory' }); +MyTischtennisUpdateHistory.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + export { User, Log, @@ -265,4 +269,5 @@ export { OfficialCompetition, OfficialCompetitionMember, MyTischtennis, + MyTischtennisUpdateHistory, }; diff --git a/backend/node_modules/.package-lock.json b/backend/node_modules/.package-lock.json index be5e579..fee7619 100644 --- a/backend/node_modules/.package-lock.json +++ b/backend/node_modules/.package-lock.json @@ -2816,6 +2816,15 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", @@ -2842,9 +2851,10 @@ } }, "node_modules/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } diff --git a/backend/node_modules/nodemailer/.ncurc.js b/backend/node_modules/nodemailer/.ncurc.js index d1db7e5..391ac07 100644 --- a/backend/node_modules/nodemailer/.ncurc.js +++ b/backend/node_modules/nodemailer/.ncurc.js @@ -1,10 +1,9 @@ +'use strict'; + module.exports = { upgrade: true, reject: [ // API changes break existing tests - 'proxy', - - // API changes - 'eslint' + 'proxy' ] }; diff --git a/backend/node_modules/nodemailer/.prettierrc.js b/backend/node_modules/nodemailer/.prettierrc.js index 3f83654..1a6faac 100644 --- a/backend/node_modules/nodemailer/.prettierrc.js +++ b/backend/node_modules/nodemailer/.prettierrc.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = { printWidth: 160, tabWidth: 4, diff --git a/backend/node_modules/nodemailer/CHANGELOG.md b/backend/node_modules/nodemailer/CHANGELOG.md index 19a95cd..04d58b4 100644 --- a/backend/node_modules/nodemailer/CHANGELOG.md +++ b/backend/node_modules/nodemailer/CHANGELOG.md @@ -1,578 +1,671 @@ # CHANGELOG +## [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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **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)) +- **license:** Updated license year ([da4744e](https://github.com/nodemailer/nodemailer/commit/da4744e491f3a68f4f68e4073684370592630e01)) ## 6.9.4 2023-07-19 -- Renamed SendinBlue to Brevo +- 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 +- 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 +- Fix uncaught exception on invalid attachment content payload ## 6.9.1 2023-01-27 -- Fix base64 encoding for emoji bytes in encoded words +- 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 +- 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) +- Add DNS timeout (huksley) +- add dns.REFUSED (lucagianfelici) ## 6.7.8 2022-08-11 -- Allow to use multiple Reply-To addresses +- Allow to use multiple Reply-To addresses ## 6.7.7 2022-07-06 -- Resolver fixes +- Resolver fixes ## 6.7.5 2022-05-04 -- No changes, pushing a new README to npmjs.org +- 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 +- 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) +- 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 +- Fix proxies for account verification ## 6.7.1 2021-11-15 -- fix verify on ses-transport (stanofsky) +- 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 +- 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+ +- 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 +- 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 */"` +- 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) +- 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) +- 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 +- Pass through textEncoding to subnodes +- Added support for AWS SES v3 SDK +- Fixed tests ## 6.4.18 2021-02-11 -- Updated README +- Updated README ## 6.4.17 2020-12-11 -- Allow mixing attachments with caendar alternatives +- Allow mixing attachments with caendar alternatives ## 6.4.16 2020-11-12 -- Applied updated prettier formating rules +- Applied updated prettier formating rules ## 6.4.15 2020-11-06 -- Minor changes in header key casing +- Minor changes in header key casing ## 6.4.14 2020-10-14 -- Disabled postinstall script +- Disabled postinstall script ## 6.4.13 2020-10-02 -- Fix normalizeHeaderKey method for single node messages +- 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] +- 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 +- 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. +- 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 +- 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] +- fix: `requeueAttempts=n` should requeue `n` times (Patrick Malouin) [a27ed2f7] ## 6.4.4 2020-03-01 -- Add `options.forceAuth` for SMTP (Patrick Malouin) [a27ed2f7] +- 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] +- 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 +- 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 +- 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] +- 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] +- 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) +- 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 +- 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 +- 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 +- Fixed regression bug with missing smtp `authMethod` property ## 6.1.0 2019-04-06 -- Added new message property `amp` for providing AMP4EMAIL content +- 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 +- 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 +- 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) +- 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 +- 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 +- 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()` +- 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] +- 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 +- Added option `skipEncoding` to JSONTransport ## 4.6.6 2018-06-10 -- Fixes mime encoded-word compatibility issue with invalid clients like Zimbra +- 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 +- 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 +- Readded logo author link to README that was accidentally removed a while ago ## 4.6.3 2018-03-13 -- Removed unneeded dependency +- Removed unneeded dependency ## 4.6.2 2018-03-06 -- When redirecting URL calls then do not include original POST content +- 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] +- 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] +- 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 +- 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 +- 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 +- 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) +- 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 +- 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 +- 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 +- Expose streamed messages size and timers in info response ## v4.1.3 2017-10-06 -- Allow generating preview links without calling createTestAccount first +- Allow generating preview links without calling createTestAccount first ## v4.1.2 2017-10-03 -- No actual changes. Needed to push updated README to npmjs +- No actual changes. Needed to push updated README to npmjs ## v4.1.1 2017-09-25 -- Fixed JSONTransport attachment handling +- 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 +- 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 +- Fixed issue with LMTP and STARTTLS ## v4.0.0 2017-04-06 -- License changed from EUPLv1.1 to MIT +- License changed from EUPLv1.1 to MIT ## v3.1.8 2017-03-21 -- Fixed invalid List-\* header generation +- Fixed invalid List-\* header generation ## v3.1.7 2017-03-14 -- Emit an error if STARTTLS ends with connection being closed +- Emit an error if STARTTLS ends with connection being closed ## v3.1.6 2017-03-14 -- Expose last server response for smtpConnection +- Expose last server response for smtpConnection ## v3.1.5 2017-03-08 -- Fixed SES transport, added missing `response` value +- 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 +- 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 +- 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 +- 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 +- 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 +- 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. +- 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) +- 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 +- Initial version of Nodemailer 3 This update brings a lot of breaking changes: -- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify about the conflicting code and I'll fix it. -- Requires **Node.js v6+** -- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes -- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible. -- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender. -- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/). +- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify about the conflicting code and I'll fix it. +- Requires **Node.js v6+** +- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes +- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible. +- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender. +- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/). And also some non-breaking changes: -- All **dependencies were dropped**. There is exactly 0 dependencies needed to use Nodemailer. This brings the installation time of Nodemailer from NPM down to less than 2 seconds -- **Delivery status notifications** added to Nodemailer -- Improved and built-in **DKIM** signing of messages. Previously you needed an external module for this and it did quite a lousy job with larger messages -- **Stream transport** to return a RFC822 formatted message as a stream. Useful if you want to use Nodemailer as a preprocessor and not for actual delivery. -- **Sendmail** transport built-in, no need for external transport plugin +- 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 +- 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 +- 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 +- Removed shrinkwrap file. Seemed to cause more trouble than help ## 2.4.1 2016-05-12 -- Fixed outdated shrinkwrap file +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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) +- 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 +- Released rc.2 as stable ## 2.0.0-rc.2 2016-01-04 -- Locked dependencies +- 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 +- 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 +- 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 +- 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 @@ -668,147 +761,147 @@ Total rewrite. See migration guide here: , (comments) and regular text for (i = 0, len = tokens.length; i < len; i++) { - token = tokens[i]; + let token = tokens[i]; + let prevToken = i ? tokens[i - 1] : null; if (token.type === 'operator') { switch (token.value) { case '<': state = 'address'; + insideQuotes = false; break; case '(': state = 'comment'; + insideQuotes = false; break; case ':': state = 'group'; isGroup = true; + insideQuotes = false; + break; + case '"': + // Track quote state for text tokens + insideQuotes = !insideQuotes; + state = 'text'; break; default: state = 'text'; + insideQuotes = false; + break; } } else if (token.value) { if (state === 'address') { @@ -46,7 +58,19 @@ function _handleAddress(tokens) { // and so will we token.value = token.value.replace(/^[^<]*<\s*/, ''); } - data[state].push(token.value); + + if (prevToken && prevToken.noBreak && data[state].length) { + // join values + data[state][data[state].length - 1] += token.value; + if (state === 'text' && insideQuotes) { + data.textWasQuoted[data.textWasQuoted.length - 1] = true; + } + } else { + data[state].push(token.value); + if (state === 'text') { + data.textWasQuoted.push(insideQuotes); + } + } } } @@ -59,16 +83,36 @@ function _handleAddress(tokens) { if (isGroup) { // http://tools.ietf.org/html/rfc2822#appendix-A.1.3 data.text = data.text.join(' '); + + // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting) + let groupMembers = []; + if (data.group.length) { + let parsedGroup = addressparser(data.group.join(',')); + // Flatten: if any member is itself a group, extract its members into the sequence + parsedGroup.forEach(member => { + if (member.group) { + // Nested group detected - flatten it by adding its members directly + groupMembers = groupMembers.concat(member.group); + } else { + groupMembers.push(member); + } + }); + } + addresses.push({ name: data.text || (address && address.name), - group: data.group.length ? addressparser(data.group.join(',')) : [] + group: groupMembers }); } else { // If no address was found, try to detect one from regular text if (!data.address.length && data.text.length) { for (i = data.text.length - 1; i >= 0; i--) { - if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) { + // Security fix: Do not extract email addresses from quoted strings + // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com + // Extracting emails from quoted text leads to misrouting vulnerabilities + if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) { data.address = data.text.splice(i, 1); + data.textWasQuoted.splice(i, 1); break; } } @@ -85,10 +129,13 @@ function _handleAddress(tokens) { // still no address if (!data.address.length) { for (i = data.text.length - 1; i >= 0; i--) { - // fixed the regex to parse email address correctly when email address has more than one @ - data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim(); - if (data.address.length) { - break; + // Security fix: Do not extract email addresses from quoted strings + if (!data.textWasQuoted[i]) { + // fixed the regex to parse email address correctly when email address has more than one @ + data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim(); + if (data.address.length) { + break; + } } } } @@ -172,11 +219,12 @@ class Tokenizer { * @return {Array} An array of operator|text tokens */ tokenize() { - let chr, - list = []; + let list = []; + for (let i = 0, len = this.str.length; i < len; i++) { - chr = this.str.charAt(i); - this.checkChar(chr); + let chr = this.str.charAt(i); + let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null; + this.checkChar(chr, nextChr); } this.list.forEach(node => { @@ -194,7 +242,7 @@ class Tokenizer { * * @param {String} chr Character from the address field */ - checkChar(chr) { + checkChar(chr, nextChr) { if (this.escaped) { // ignore next condition blocks } else if (chr === this.operatorExpecting) { @@ -202,10 +250,16 @@ class Tokenizer { 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 = { diff --git a/backend/node_modules/nodemailer/lib/base64/index.js b/backend/node_modules/nodemailer/lib/base64/index.js index cafd5d8..c23becc 100644 --- a/backend/node_modules/nodemailer/lib/base64/index.js +++ b/backend/node_modules/nodemailer/lib/base64/index.js @@ -35,15 +35,12 @@ function wrap(str, lineLength) { let pos = 0; let chunkLength = lineLength * 1024; while (pos < str.length) { - let wrappedLines = str - .substr(pos, chunkLength) - .replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n') - .trim(); + let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n'); result.push(wrappedLines); pos += chunkLength; } - return result.join('\r\n').trim(); + return result.join(''); } /** @@ -56,7 +53,6 @@ function wrap(str, lineLength) { class Encoder extends Transform { constructor(options) { super(); - // init Transform this.options = options || {}; if (this.options.lineLength !== false) { @@ -98,17 +94,20 @@ class Encoder extends Transform { if (this.options.lineLength) { b64 = wrap(b64, this.options.lineLength); - // remove last line as it is still most probably incomplete let lastLF = b64.lastIndexOf('\n'); if (lastLF < 0) { this._curLine = b64; b64 = ''; - } else if (lastLF === b64.length - 1) { - this._curLine = ''; } else { - this._curLine = b64.substr(lastLF + 1); - b64 = b64.substr(0, lastLF + 1); + 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) { @@ -125,16 +124,14 @@ class Encoder extends Transform { } if (this._curLine) { - this._curLine = wrap(this._curLine, this.options.lineLength); this.outputBytes += this._curLine.length; - this.push(this._curLine, 'ascii'); + this.push(Buffer.from(this._curLine, 'ascii')); this._curLine = ''; } done(); } } -// expose to the world module.exports = { encode, wrap, diff --git a/backend/node_modules/nodemailer/lib/dkim/index.js b/backend/node_modules/nodemailer/lib/dkim/index.js index 7536b37..e468652 100644 --- a/backend/node_modules/nodemailer/lib/dkim/index.js +++ b/backend/node_modules/nodemailer/lib/dkim/index.js @@ -12,7 +12,7 @@ const path = require('path'); const crypto = require('crypto'); const DKIM_ALGO = 'sha256'; -const MAX_MESSAGE_SIZE = 128 * 1024; // buffer messages larger than this to disk +const MAX_MESSAGE_SIZE = 2 * 1024 * 1024; // buffer messages larger than this to disk /* // Usage: @@ -42,7 +42,9 @@ class DKIMSigner { 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.cachePath = this.cacheDir + ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) + : false; this.cache = false; this.headers = false; diff --git a/backend/node_modules/nodemailer/lib/dkim/sign.js b/backend/node_modules/nodemailer/lib/dkim/sign.js index 70943b6..4a103a4 100644 --- a/backend/node_modules/nodemailer/lib/dkim/sign.js +++ b/backend/node_modules/nodemailer/lib/dkim/sign.js @@ -41,7 +41,7 @@ module.exports = (headers, hashAlgo, bodyHash, options) => { signer.update(canonicalizedHeaderData.headers); try { signature = signer.sign(options.privateKey, 'base64'); - } catch (E) { + } catch (_E) { return false; } diff --git a/backend/node_modules/nodemailer/lib/fetch/index.js b/backend/node_modules/nodemailer/lib/fetch/index.js index 2e73dbd..3c918ce 100644 --- a/backend/node_modules/nodemailer/lib/fetch/index.js +++ b/backend/node_modules/nodemailer/lib/fetch/index.js @@ -132,7 +132,13 @@ function nmfetch(url, options) { }); } - if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) { + if ( + parsed.protocol === 'https:' && + parsed.hostname && + parsed.hostname !== reqOptions.host && + !net.isIP(parsed.hostname) && + !reqOptions.servername + ) { reqOptions.servername = parsed.hostname; } diff --git a/backend/node_modules/nodemailer/lib/mail-composer/index.js b/backend/node_modules/nodemailer/lib/mail-composer/index.js index 7e90215..66099aa 100644 --- a/backend/node_modules/nodemailer/lib/mail-composer/index.js +++ b/backend/node_modules/nodemailer/lib/mail-composer/index.js @@ -86,20 +86,34 @@ class MailComposer { let icalEvent, eventObject; let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => { let data; - let isMessageNode = /^message\//i.test(attachment.contentType); if (/^data:/i.test(attachment.path || attachment.href)) { attachment = this._processDataUrl(attachment); } - let contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'); + let contentType = + attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'); + let isImage = /^image\//i.test(contentType); - let contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment'); + let isMessageNode = /^message\//i.test(contentType); + + let contentDisposition = + attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment'); + + let contentTransferEncoding; + if ('contentTransferEncoding' in attachment) { + // also contains `false`, to set + contentTransferEncoding = attachment.contentTransferEncoding; + } else if (isMessageNode) { + contentTransferEncoding = '7bit'; + } else { + contentTransferEncoding = 'base64'; // the default + } data = { contentType, contentDisposition, - contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64' + contentTransferEncoding }; if (attachment.filename) { @@ -200,7 +214,10 @@ class MailComposer { 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)) { + 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 = { @@ -225,7 +242,10 @@ class MailComposer { } 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)) { + 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 = { @@ -260,14 +280,18 @@ class MailComposer { } eventObject.filename = false; - eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase(); + 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)) { + 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 = { @@ -292,7 +316,9 @@ class MailComposer { } data = { - contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'), + contentType: + alternative.contentType || + mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'), contentTransferEncoding: alternative.contentTransferEncoding }; @@ -538,9 +564,33 @@ class MailComposer { * @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 > 100000) { + // 100KB limit for data URL string + // Return empty content for excessively long data URLs + return Object.assign({}, element, { + path: false, + href: false, + content: Buffer.alloc(0), + contentType: element.contentType || 'application/octet-stream' + }); + } + let parsedDataUri; - if ((element.path || element.href).match(/^data:/)) { - parsedDataUri = parseDataURI(element.path || element.href); + try { + parsedDataUri = parseDataURI(dataUrl); + } catch (_err) { + return element; } if (!parsedDataUri) { diff --git a/backend/node_modules/nodemailer/lib/mailer/index.js b/backend/node_modules/nodemailer/lib/mailer/index.js index 3c80184..6da313e 100644 --- a/backend/node_modules/nodemailer/lib/mailer/index.js +++ b/backend/node_modules/nodemailer/lib/mailer/index.js @@ -87,6 +87,11 @@ class Mail extends EventEmitter { 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); + }); } /** @@ -236,7 +241,14 @@ class Mail extends EventEmitter { } getVersionString() { - return util.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version); + return util.format( + '%s (%s; +%s; %s/%s)', + packageData.name, + packageData.version, + packageData.homepage, + this.transporter.name, + this.transporter.version + ); } _processPlugins(step, mail, callback) { diff --git a/backend/node_modules/nodemailer/lib/mailer/mail-message.js b/backend/node_modules/nodemailer/lib/mailer/mail-message.js index 24d492b..e089c10 100644 --- a/backend/node_modules/nodemailer/lib/mailer/mail-message.js +++ b/backend/node_modules/nodemailer/lib/mailer/mail-message.js @@ -64,7 +64,8 @@ class MailMessage { 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); + attachment.filename = + (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1); if (attachment.filename.indexOf('.') < 0) { attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType); } diff --git a/backend/node_modules/nodemailer/lib/mime-funcs/index.js b/backend/node_modules/nodemailer/lib/mime-funcs/index.js index 86e975d..566bcaa 100644 --- a/backend/node_modules/nodemailer/lib/mime-funcs/index.js +++ b/backend/node_modules/nodemailer/lib/mime-funcs/index.js @@ -269,7 +269,7 @@ module.exports = { // first line includes the charset and language info and needs to be encoded // even if it does not contain any unicode characters - line = 'utf-8\x27\x27'; + line = "utf-8''"; let encoded = true; startPos = 0; @@ -614,7 +614,7 @@ module.exports = { try { // might throw if we try to encode invalid sequences, eg. partial emoji str = encodeURIComponent(str); - } catch (E) { + } catch (_E) { // should never run return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, ''); } diff --git a/backend/node_modules/nodemailer/lib/mime-funcs/mime-types.js b/backend/node_modules/nodemailer/lib/mime-funcs/mime-types.js index 1e9a220..eed3cd6 100644 --- a/backend/node_modules/nodemailer/lib/mime-funcs/mime-types.js +++ b/backend/node_modules/nodemailer/lib/mime-funcs/mime-types.js @@ -44,6 +44,7 @@ const mimeTypes = new Map([ ['application/fractals', 'fif'], ['application/freeloader', 'frl'], ['application/futuresplash', 'spl'], + ['application/geo+json', 'geojson'], ['application/gnutar', 'tgz'], ['application/groupwise', 'vew'], ['application/hlp', 'hlp'], @@ -1101,7 +1102,10 @@ const extensions = new Map([ ['bdm', 'application/vnd.syncml.dm+wbxml'], ['bed', 'application/vnd.realvnc.bed'], ['bh2', 'application/vnd.fujitsu.oasysprs'], - ['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']], + [ + 'bin', + ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary'] + ], ['bm', 'image/bmp'], ['bmi', 'application/vnd.bmi'], ['bmp', ['image/bmp', 'image/x-windows-bmp']], @@ -1146,7 +1150,10 @@ const extensions = new Map([ ['cii', 'application/vnd.anser-web-certificate-issue-initiation'], ['cil', 'application/vnd.ms-artgalry'], ['cla', 'application/vnd.claymore'], - ['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']], + [ + 'class', + ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class'] + ], ['clkk', 'application/vnd.crick.clicker.keyboard'], ['clkp', 'application/vnd.crick.clicker.palette'], ['clkt', 'application/vnd.crick.clicker.template'], @@ -1287,6 +1294,7 @@ const extensions = new Map([ ['gac', 'application/vnd.groove-account'], ['gdl', 'model/vnd.gdl'], ['geo', 'application/vnd.dynageo'], + ['geojson', 'application/geo+json'], ['gex', 'application/vnd.geometry-explorer'], ['ggb', 'application/vnd.geogebra.file'], ['ggt', 'application/vnd.geogebra.tool'], @@ -1750,7 +1758,10 @@ const extensions = new Map([ ['sbml', 'application/sbml+xml'], ['sc', 'application/vnd.ibm.secure-container'], ['scd', 'application/x-msschedule'], - ['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']], + [ + 'scm', + ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme'] + ], ['scq', 'application/scvp-cv-request'], ['scs', 'application/scvp-cv-response'], ['sct', 'text/scriptlet'], diff --git a/backend/node_modules/nodemailer/lib/mime-node/index.js b/backend/node_modules/nodemailer/lib/mime-node/index.js index b92fd7a..d815543 100644 --- a/backend/node_modules/nodemailer/lib/mime-node/index.js +++ b/backend/node_modules/nodemailer/lib/mime-node/index.js @@ -552,7 +552,11 @@ class MimeNode { this._handleContentType(structured); - if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) { + if ( + structured.value.match(/^text\/plain\b/) && + typeof this.content === 'string' && + /[\u0080-\uFFFF]/.test(this.content) + ) { structured.params.charset = 'utf-8'; } @@ -963,8 +967,8 @@ class MimeNode { setImmediate(() => { try { contentStream.end(content._resolvedValue); - } catch (err) { - contentStream.emit('error', err); + } catch (_err) { + contentStream.emit('error', _err); } }); @@ -995,8 +999,8 @@ class MimeNode { setImmediate(() => { try { contentStream.end(content || ''); - } catch (err) { - contentStream.emit('error', err); + } catch (_err) { + contentStream.emit('error', _err); } }); return contentStream; @@ -1014,7 +1018,6 @@ class MimeNode { return [].concat.apply( [], [].concat(addresses).map(address => { - // eslint-disable-line prefer-spread if (address && address.address) { address.address = this._normalizeAddress(address.address); address.name = address.name || ''; @@ -1113,7 +1116,6 @@ class MimeNode { .apply( [], [].concat(value || '').map(elm => { - // eslint-disable-line prefer-spread elm = (elm || '') .toString() .replace(/\r?\n|\r/g, ' ') @@ -1219,7 +1221,7 @@ class MimeNode { try { encodedDomain = punycode.toASCII(domain.toLowerCase()); - } catch (err) { + } catch (_err) { // keep as is? } @@ -1282,7 +1284,7 @@ class MimeNode { // count latin alphabet symbols and 8-bit range symbols + control symbols // if there are more latin characters, then use quoted-printable // encoding, otherwise use base64 - nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex + nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; latinLen = (value.match(/[a-z]/gi) || []).length; // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B encoding = nonLatinLen < latinLen ? 'Q' : 'B'; diff --git a/backend/node_modules/nodemailer/lib/nodemailer.js b/backend/node_modules/nodemailer/lib/nodemailer.js index 9d43375..d8dcdb9 100644 --- a/backend/node_modules/nodemailer/lib/nodemailer.js +++ b/backend/node_modules/nodemailer/lib/nodemailer.js @@ -45,6 +45,13 @@ module.exports.createTransport = function (transporter, defaults) { } else if (options.jsonTransport) { transporter = new JSONTransport(options); } else if (options.SES) { + if (options.SES.ses && options.SES.aws) { + let error = new Error( + 'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/' + ); + error.code = 'LegacyConfig'; + throw error; + } transporter = new SESTransport(options); } else { transporter = new SMTPTransport(options); diff --git a/backend/node_modules/nodemailer/lib/qp/index.js b/backend/node_modules/nodemailer/lib/qp/index.js index 6bf6f08..c6a7add 100644 --- a/backend/node_modules/nodemailer/lib/qp/index.js +++ b/backend/node_modules/nodemailer/lib/qp/index.js @@ -28,7 +28,10 @@ function encode(buffer) { for (let i = 0, len = buffer.length; i < len; i++) { ord = buffer[i]; // if the char is in allowed range, then keep as is, unless it is a WS in the end of a line - if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) { + if ( + checkRanges(ord, ranges) && + !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d)) + ) { result += String.fromCharCode(ord); continue; } @@ -90,7 +93,12 @@ function wrap(str, lineLength) { } // 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))) { + 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; diff --git a/backend/node_modules/nodemailer/lib/ses-transport/index.js b/backend/node_modules/nodemailer/lib/ses-transport/index.js index 6646a4a..d2e2ccf 100644 --- a/backend/node_modules/nodemailer/lib/ses-transport/index.js +++ b/backend/node_modules/nodemailer/lib/ses-transport/index.js @@ -4,15 +4,11 @@ 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 * - * Possible options can be the following: - * - * * **sendingRate** optional Number specifying how many messages per second should be delivered to SES - * * **maxConnections** optional Number specifying max number of parallel connections to SES - * * @constructor * @param {Object} optional config parameter */ @@ -30,119 +26,17 @@ class SESTransport extends EventEmitter { this.logger = shared.getLogger(this.options, { component: this.options.component || 'ses-transport' }); - - // parallel sending connections - this.maxConnections = Number(this.options.maxConnections) || Infinity; - this.connections = 0; - - // max messages per second - this.sendingRate = Number(this.options.sendingRate) || Infinity; - this.sendingRateTTL = null; - this.rateInterval = 1000; // milliseconds - this.rateMessages = []; - - this.pending = []; - - this.idling = true; - - setImmediate(() => { - if (this.idling) { - this.emit('idle'); - } - }); } - /** - * Schedules a sending of a message - * - * @param {Object} emailMessage MailComposer object - * @param {Function} callback Callback function to run when the sending is completed - */ - send(mail, callback) { - if (this.connections >= this.maxConnections) { - this.idling = false; - return this.pending.push({ - mail, - callback - }); + 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)); } - - if (!this._checkSendingRate()) { - this.idling = false; - return this.pending.push({ - mail, - callback - }); - } - - this._send(mail, (...args) => { - setImmediate(() => callback(...args)); - this._sent(); - }); - } - - _checkRatedQueue() { - if (this.connections >= this.maxConnections || !this._checkSendingRate()) { - return; - } - - if (!this.pending.length) { - if (!this.idling) { - this.idling = true; - this.emit('idle'); - } - return; - } - - let next = this.pending.shift(); - this._send(next.mail, (...args) => { - setImmediate(() => next.callback(...args)); - this._sent(); - }); - } - - _checkSendingRate() { - clearTimeout(this.sendingRateTTL); - - let now = Date.now(); - let oldest = false; - // delete older messages - for (let i = this.rateMessages.length - 1; i >= 0; i--) { - if (this.rateMessages[i].ts >= now - this.rateInterval && (!oldest || this.rateMessages[i].ts < oldest)) { - oldest = this.rateMessages[i].ts; - } - - if (this.rateMessages[i].ts < now - this.rateInterval && !this.rateMessages[i].pending) { - this.rateMessages.splice(i, 1); - } - } - - if (this.rateMessages.length < this.sendingRate) { - return true; - } - - let delay = Math.max(oldest + 1001, now + 20); - this.sendingRateTTL = setTimeout(() => this._checkRatedQueue(), now - delay); - - try { - this.sendingRateTTL.unref(); - } catch (E) { - // Ignore. Happens on envs with non-node timer implementation - } - - return false; - } - - _sent() { - this.connections--; - this._checkRatedQueue(); - } - - /** - * Returns true if there are free slots in the queue - */ - isIdle() { - return this.idling; + return cb(null, false); } /** @@ -151,13 +45,17 @@ class SESTransport extends EventEmitter { * @param {Object} emailMessage MailComposer object * @param {Function} callback Callback function to run when the sending is completed */ - _send(mail, callback) { + send(mail, callback) { let statObject = { ts: Date.now(), pending: true }; - this.connections++; - this.rateMessages.push(statObject); + + let fromHeader = mail.message._headers.find(header => /^from$/i.test(header.key)); + if (fromHeader) { + let mimeNode = new MimeNode('text/plain'); + fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value)); + } let envelope = mail.data.envelope || mail.message.getEnvelope(); let messageId = mail.message.messageId(); @@ -227,45 +125,29 @@ class SESTransport extends EventEmitter { } let sesMessage = { - RawMessage: { - // required - Data: raw // required + Content: { + Raw: { + // required + Data: raw // required + } }, - Source: envelope.from, - Destinations: envelope.to + FromEmailAddress: fromHeader ? fromHeader : envelope.from, + Destination: { + ToAddresses: envelope.to + } }; Object.keys(mail.data.ses || {}).forEach(key => { sesMessage[key] = mail.data.ses[key]; }); - let ses = (this.ses.aws ? this.ses.ses : this.ses) || {}; - let aws = this.ses.aws || {}; - - let getRegion = cb => { - if (ses.config && typeof ses.config.region === 'function') { - // promise - return ses.config - .region() - .then(region => cb(null, region)) - .catch(err => cb(err)); - } - return cb(null, (ses.config && ses.config.region) || 'us-east-1'); - }; - - getRegion((err, region) => { + this.getRegion((err, region) => { if (err || !region) { region = 'us-east-1'; } - let sendPromise; - if (typeof ses.send === 'function' && aws.SendRawEmailCommand) { - // v3 API - sendPromise = ses.send(new aws.SendRawEmailCommand(sesMessage)); - } else { - // v2 API - sendPromise = ses.sendRawEmail(sesMessage).promise(); - } + const command = new this.ses.SendEmailCommand(sesMessage); + const sendPromise = this.ses.sesClient.send(command); sendPromise .then(data => { @@ -273,7 +155,7 @@ class SESTransport extends EventEmitter { region = 'email'; } - statObject.pending = false; + statObject.pending = true; callback(null, { envelope: { from: envelope.from, @@ -309,38 +191,41 @@ class SESTransport extends EventEmitter { */ verify(callback) { let promise; - let ses = (this.ses.aws ? this.ses.ses : this.ses) || {}; - let aws = this.ses.aws || {}; - - const sesMessage = { - RawMessage: { - // required - Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid' - }, - Source: 'invalid@invalid', - Destinations: ['invalid@invalid'] - }; - if (!callback) { promise = new Promise((resolve, reject) => { callback = shared.callbackPromise(resolve, reject); }); } + const cb = err => { - if (err && (err.code || err.Code) !== 'InvalidParameterValue') { + if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) { return callback(err); } return callback(null, true); }; - if (typeof ses.send === 'function' && aws.SendRawEmailCommand) { - // v3 API - sesMessage.RawMessage.Data = Buffer.from(sesMessage.RawMessage.Data); - ses.send(new aws.SendRawEmailCommand(sesMessage), cb); - } else { - // v2 API - ses.sendRawEmail(sesMessage, cb); - } + const sesMessage = { + Content: { + Raw: { + Data: Buffer.from('From: \r\nTo: \r\n Subject: Invalid\r\n\r\nInvalid') + } + }, + FromEmailAddress: 'invalid@invalid', + Destination: { + ToAddresses: ['invalid@invalid'] + } + }; + + this.getRegion((err, region) => { + if (err || !region) { + region = 'us-east-1'; + } + + const command = new this.ses.SendEmailCommand(sesMessage); + const sendPromise = this.ses.sesClient.send(command); + + sendPromise.then(data => cb(null, data)).catch(err => cb(err)); + }); return promise; } diff --git a/backend/node_modules/nodemailer/lib/shared/index.js b/backend/node_modules/nodemailer/lib/shared/index.js index 4c16a37..d6fced6 100644 --- a/backend/node_modules/nodemailer/lib/shared/index.js +++ b/backend/node_modules/nodemailer/lib/shared/index.js @@ -11,11 +11,19 @@ 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) { +} catch (_err) { // fails on some systems } @@ -81,8 +89,8 @@ const formatDNSValue = (value, extra) => { !value.addresses || !value.addresses.length ? null : value.addresses.length === 1 - ? value.addresses[0] - : value.addresses[Math.floor(Math.random() * value.addresses.length)] + ? value.addresses[0] + : value.addresses[Math.floor(Math.random() * value.addresses.length)] }, extra || {} ); @@ -113,7 +121,27 @@ module.exports.resolveHostname = (options, callback) => { if (dnsCache.has(options.host)) { cached = dnsCache.get(options.host); - if (!cached.expires || cached.expires >= Date.now()) { + // 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, { @@ -126,7 +154,11 @@ module.exports.resolveHostname = (options, callback) => { resolver(4, options.host, options, (err, addresses) => { if (err) { if (cached) { - // ignore error, use expired value + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + return callback( null, formatDNSValue(cached.value, { @@ -160,7 +192,11 @@ module.exports.resolveHostname = (options, callback) => { resolver(6, options.host, options, (err, addresses) => { if (err) { if (cached) { - // ignore error, use expired value + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + return callback( null, formatDNSValue(cached.value, { @@ -195,7 +231,11 @@ module.exports.resolveHostname = (options, callback) => { dns.lookup(options.host, { all: true }, (err, addresses) => { if (err) { if (cached) { - // ignore error, use expired value + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + return callback( null, formatDNSValue(cached.value, { @@ -246,9 +286,13 @@ module.exports.resolveHostname = (options, callback) => { }) ); }); - } catch (err) { + } catch (_err) { if (cached) { - // ignore error, use expired value + dnsCache.set(options.host, { + value: cached.value, + expires: Date.now() + (options.dnsTtl || DNS_TTL) + }); + return callback( null, formatDNSValue(cached.value, { @@ -419,52 +463,74 @@ module.exports.callbackPromise = (resolve, reject) => }; module.exports.parseDataURI = uri => { - let input = uri; - let commaPos = input.indexOf(','); - if (!commaPos) { - return uri; + if (typeof uri !== 'string') { + return null; } - let data = input.substring(commaPos + 1); - let metaStr = input.substring('data:'.length, commaPos); + // 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(';'); - let metaEntries = metaStr.split(';'); - let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false; - if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) { - encoding = lastMetaEntry.toLowerCase(); - metaEntries.pop(); - } - - let contentType = metaEntries.shift() || 'application/octet-stream'; - let params = {}; - for (let entry of metaEntries) { - let sep = entry.indexOf('='); - if (sep >= 0) { - let key = entry.substring(0, sep); - let value = entry.substring(sep + 1); - params[key] = value; + 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(); } } - switch (encoding) { - case 'base64': - data = Buffer.from(data, 'base64'); - break; - case 'utf8': - data = Buffer.from(data); - break; - default: - try { - data = Buffer.from(decodeURIComponent(data)); - } catch (err) { - data = Buffer.from(data); + 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; } - data = Buffer.from(data); + } } - return { data, encoding, contentType, params }; + // 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 + }; }; /** diff --git a/backend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js b/backend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js index a59bb49..64819b9 100644 --- a/backend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +++ b/backend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js @@ -51,7 +51,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) { finished = true; try { socket.destroy(); - } catch (E) { + } catch (_E) { // ignore } callback(err); @@ -118,7 +118,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) { if (!match || (match[1] || '').charAt(0) !== '2') { try { socket.destroy(); - } catch (E) { + } catch (_E) { // ignore } return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''))); diff --git a/backend/node_modules/nodemailer/lib/smtp-connection/index.js b/backend/node_modules/nodemailer/lib/smtp-connection/index.js index e7ef188..0d7aad6 100644 --- a/backend/node_modules/nodemailer/lib/smtp-connection/index.js +++ b/backend/node_modules/nodemailer/lib/smtp-connection/index.js @@ -124,7 +124,7 @@ class SMTPConnection extends EventEmitter { /** * The socket connecting to the server - * @publick + * @public */ this._socket = false; @@ -243,6 +243,8 @@ class SMTPConnection extends EventEmitter { if (this.options.connection) { // connection is already opened this._socket = this.options.connection; + setupConnectionHandlers(); + if (this.secureConnection && !this.alreadySecured) { setImmediate(() => this._upgradeConnection(err => { @@ -412,8 +414,8 @@ class SMTPConnection extends EventEmitter { if (socket && !socket.destroyed) { try { - this._socket[closeMethod](); - } catch (E) { + socket[closeMethod](); + } catch (_E) { // just ignore } } @@ -628,6 +630,15 @@ class SMTPConnection extends EventEmitter { let startTime = Date.now(); this._setEnvelope(envelope, (err, info) => { if (err) { + // create passthrough stream to consume to prevent OOM + let stream = new PassThrough(); + if (typeof message.pipe === 'function') { + message.pipe(stream); + } else { + stream.write(message); + stream.end(); + } + return callback(err); } let envelopeTime = Date.now(); @@ -1283,7 +1294,12 @@ class SMTPConnection extends EventEmitter { if (str.charAt(0) !== '2') { if (this.options.requireTLS) { - this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO'); + this._onError( + new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), + 'ECONNECTION', + str, + 'EHLO' + ); return; } @@ -1465,7 +1481,9 @@ class SMTPConnection extends EventEmitter { let challengeString = ''; if (!challengeMatch) { - return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')); + return callback( + this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5') + ); } else { challengeString = challengeMatch[1]; } @@ -1608,7 +1626,7 @@ class SMTPConnection extends EventEmitter { } if (!this._envelope.rcptQueue.length) { - return callback(this._formatError('Can\x27t send mail - no recipients defined', 'EENVELOPE', false, 'API')); + return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API')); } else { this._recipientQueue = []; @@ -1664,7 +1682,7 @@ class SMTPConnection extends EventEmitter { }); this._sendCommand('DATA'); } else { - err = this._formatError('Can\x27t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO'); + err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO'); err.rejected = this._envelope.rejected; err.rejectedErrors = this._envelope.rejectedErrors; return callback(err); @@ -1803,7 +1821,7 @@ class SMTPConnection extends EventEmitter { let defaultHostname; try { defaultHostname = os.hostname() || ''; - } catch (err) { + } catch (_err) { // fails on windows 7 defaultHostname = 'localhost'; } diff --git a/backend/node_modules/nodemailer/lib/smtp-pool/index.js b/backend/node_modules/nodemailer/lib/smtp-pool/index.js index 6a5d309..1c16a7f 100644 --- a/backend/node_modules/nodemailer/lib/smtp-pool/index.js +++ b/backend/node_modules/nodemailer/lib/smtp-pool/index.js @@ -406,6 +406,10 @@ class SMTPPool extends EventEmitter { this._continueProcessing(); }, 50); } else { + if (!this._closed && this.idling && !this._connections.length) { + this.emit('clear'); + } + this._continueProcessing(); } }); diff --git a/backend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js b/backend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js index d67cc5c..eae6409 100644 --- a/backend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +++ b/backend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js @@ -23,7 +23,8 @@ class PoolResource extends EventEmitter { switch ((this.options.auth.type || '').toString().toUpperCase()) { case 'OAUTH2': { let oauth2 = new XOAuth2(this.options.auth, this.logger); - oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback; + oauth2.provisionCallback = + (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback; this.auth = { type: 'OAUTH2', user: this.options.auth.user, @@ -127,7 +128,7 @@ class PoolResource extends EventEmitter { try { timer.unref(); - } catch (E) { + } catch (_E) { // Ignore. Happens on envs with non-node timer implementation } }); diff --git a/backend/node_modules/nodemailer/lib/smtp-transport/index.js b/backend/node_modules/nodemailer/lib/smtp-transport/index.js index a1c45a5..7469ee0 100644 --- a/backend/node_modules/nodemailer/lib/smtp-transport/index.js +++ b/backend/node_modules/nodemailer/lib/smtp-transport/index.js @@ -197,7 +197,7 @@ class SMTPTransport extends EventEmitter { try { timer.unref(); - } catch (E) { + } catch (_E) { // Ignore. Happens on envs with non-node timer implementation } }); diff --git a/backend/node_modules/nodemailer/lib/well-known/services.json b/backend/node_modules/nodemailer/lib/well-known/services.json index e80c0b2..89e49f4 100644 --- a/backend/node_modules/nodemailer/lib/well-known/services.json +++ b/backend/node_modules/nodemailer/lib/well-known/services.json @@ -1,63 +1,120 @@ { "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 }, - "Forward Email": { - "aliases": ["FE", "ForwardEmail"], - "domains": ["forwardemail.net"], - "host": "smtp.forwardemail.net", - "port": 465, - "secure": true - }, - "Feishu Mail": { + "description": "Feishu Mail (Lark)", "aliases": ["Feishu", "FeishuMail"], "domains": ["www.feishu.cn"], "host": "smtp.feishu.cn", @@ -65,13 +122,24 @@ "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", @@ -79,26 +147,38 @@ "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", @@ -106,6 +186,7 @@ }, "iCloud": { + "description": "iCloud Mail", "aliases": ["Me", "Mac"], "domains": ["me.com", "mac.com"], "host": "smtp.mail.me.com", @@ -113,72 +194,117 @@ }, "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, @@ -186,30 +312,37 @@ }, "Outlook365": { + "description": "Microsoft 365 / Office 365", "host": "smtp.office365.com", "port": 587, "secure": false }, - "OhMySMTP": { - "host": "smtp.ohmysmtp.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, @@ -217,6 +350,7 @@ }, "QQex": { + "description": "QQ Enterprise Mail", "aliases": ["QQ Enterprise"], "domains": ["exmail.qq.com"], "host": "smtp.exmail.qq.com", @@ -224,89 +358,204 @@ "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-US-EAST-1": { - "host": "email-smtp.us-east-1.amazonaws.com", - "port": 465, - "secure": true - }, - - "SES-US-WEST-2": { - "host": "email-smtp.us-west-2.amazonaws.com", - "port": 465, - "secure": true - }, - - "SES-EU-WEST-1": { - "host": "email-smtp.eu-west-1.amazonaws.com", - "port": 465, - "secure": true - }, - - "SES-AP-SOUTH-1": { - "host": "email-smtp.ap-south-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", @@ -315,11 +564,21 @@ }, "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, @@ -327,28 +586,26 @@ }, "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" - }, - - "126": { - "host": "smtp.126.com", - "port": 465, - "secure": true - }, - - "163": { - "host": "smtp.163.com", - "port": 465, - "secure": true } } diff --git a/backend/node_modules/nodemailer/lib/xoauth2/index.js b/backend/node_modules/nodemailer/lib/xoauth2/index.js index ed461df..e4748c0 100644 --- a/backend/node_modules/nodemailer/lib/xoauth2/index.js +++ b/backend/node_modules/nodemailer/lib/xoauth2/index.js @@ -72,6 +72,9 @@ class XOAuth2 extends Stream { let timeout = Math.max(Number(this.options.timeout) || 0, 0); this.expires = (timeout && Date.now() + timeout * 1000) || 0; } + + this.renewing = false; // Track if renewal is in progress + this.renewalQueue = []; // Queue for pending requests during renewal } /** @@ -82,14 +85,61 @@ class XOAuth2 extends Stream { */ 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); } - let generateCallback = (...args) => { - if (args[0]) { + // 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 + ); + return callback(new Error("Can't create new access token for user")); + } + + // 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: args[0], + err, tnx: 'OAUTH2', user: this.options.user, action: 'renew' @@ -108,7 +158,8 @@ class XOAuth2 extends Stream { this.options.user ); } - callback(...args); + // Complete original request + callback(err, accessToken); }; if (this.provisionCallback) { @@ -166,8 +217,8 @@ class XOAuth2 extends Stream { let token; try { token = this.jwtSignRS256(tokenData); - } catch (err) { - return callback(new Error('Can\x27t generate token. Check your auth options')); + } catch (_err) { + return callback(new Error("Can't generate token. Check your auth options")); } urlOptions = { @@ -181,7 +232,7 @@ class XOAuth2 extends Stream { }; } else { if (!this.options.refreshToken) { - return callback(new Error('Can\x27t create new access token for user')); + return callback(new Error("Can't create new access token for user")); } // web app - https://developers.google.com/identity/protocols/OAuth2WebServer diff --git a/backend/node_modules/nodemailer/package.json b/backend/node_modules/nodemailer/package.json index 0e0f728..02b069a 100644 --- a/backend/node_modules/nodemailer/package.json +++ b/backend/node_modules/nodemailer/package.json @@ -1,12 +1,15 @@ { "name": "nodemailer", - "version": "6.9.14", + "version": "7.0.9", "description": "Easy as cake e-mail sending from your Node.js applications", "main": "lib/nodemailer.js", "scripts": { "test": "node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js", "test:coverage": "c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js", + "format": "prettier --write \"**/*.{js,json,md}\"", + "format:check": "prettier --check \"**/*.{js,json,md}\"", "lint": "eslint .", + "lint:fix": "eslint . --fix", "update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install" }, "repository": { @@ -23,19 +26,20 @@ }, "homepage": "https://nodemailer.com/", "devDependencies": { - "@aws-sdk/client-ses": "3.600.0", + "@aws-sdk/client-sesv2": "3.901.0", "bunyan": "1.8.15", - "c8": "10.1.2", - "eslint": "8.57.0", - "eslint-config-nodemailer": "1.2.0", - "eslint-config-prettier": "9.1.0", + "c8": "10.1.3", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^16.4.0", "libbase64": "1.3.0", - "libmime": "5.3.5", - "libqp": "2.1.0", + "libmime": "5.3.7", + "libqp": "2.1.1", "nodemailer-ntlm-auth": "1.0.4", + "prettier": "^3.6.2", "proxy": "1.0.2", "proxy-test-server": "1.0.0", - "smtp-server": "3.13.4" + "smtp-server": "3.14.0" }, "engines": { "node": ">=6.0.0" diff --git a/backend/package-lock.json b/backend/package-lock.json index 622f34b..9263645 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,7 +22,8 @@ "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.3", - "nodemailer": "^6.9.14", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.9", "pdf-parse": "^1.1.1", "sequelize": "^6.37.3", "sharp": "^0.33.5" @@ -2827,6 +2828,15 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-ensure": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", @@ -2853,9 +2863,10 @@ } }, "node_modules/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } diff --git a/backend/package.json b/backend/package.json index d8d7371..9a6c9fa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,8 @@ "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "mysql2": "^3.10.3", - "nodemailer": "^6.9.14", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.9", "pdf-parse": "^1.1.1", "sequelize": "^6.37.3", "sharp": "^0.33.5" diff --git a/backend/routes/myTischtennisRoutes.js b/backend/routes/myTischtennisRoutes.js index 2a3c668..1d02f3b 100644 --- a/backend/routes/myTischtennisRoutes.js +++ b/backend/routes/myTischtennisRoutes.js @@ -25,5 +25,8 @@ router.post('/verify', myTischtennisController.verifyLogin); // GET /api/mytischtennis/session - Get stored session router.get('/session', myTischtennisController.getSession); +// GET /api/mytischtennis/update-history - Get update ratings history +router.get('/update-history', myTischtennisController.getUpdateHistory); + export default router; diff --git a/backend/server.js b/backend/server.js index ea4a109..ac74e26 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,7 +8,7 @@ import { DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group, GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult, - TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis + TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, MyTischtennisUpdateHistory } from './models/index.js'; import authRoutes from './routes/authRoutes.js'; import clubRoutes from './routes/clubRoutes.js'; @@ -38,6 +38,7 @@ import teamRoutes from './routes/teamRoutes.js'; import clubTeamRoutes from './routes/clubTeamRoutes.js'; import teamDocumentRoutes from './routes/teamDocumentRoutes.js'; import seasonRoutes from './routes/seasonRoutes.js'; +import schedulerService from './services/schedulerService.js'; const app = express(); const port = process.env.PORT || 3000; @@ -187,9 +188,14 @@ app.get('*', (req, res) => { await safeSync(Accident); await safeSync(UserToken); await safeSync(MyTischtennis); + await safeSync(MyTischtennisUpdateHistory); + + // Start scheduler service + schedulerService.start(); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); + console.log('Scheduler service started - Rating updates scheduled for 6:00 AM daily'); }); } catch (err) { console.error('Unable to synchronize the database:', err); diff --git a/backend/services/autoUpdateRatingsService.js b/backend/services/autoUpdateRatingsService.js new file mode 100644 index 0000000..daeff9d --- /dev/null +++ b/backend/services/autoUpdateRatingsService.js @@ -0,0 +1,141 @@ +import myTischtennisService from './myTischtennisService.js'; +import myTischtennisClient from '../clients/myTischtennisClient.js'; +import MyTischtennis from '../models/MyTischtennis.js'; +import { devLog } from '../utils/logger.js'; + +class AutoUpdateRatingsService { + /** + * Execute automatic rating updates for all users with enabled auto-updates + */ + async executeAutomaticUpdates() { + devLog('Starting automatic rating updates...'); + + try { + // Find all users with auto-updates enabled + const accounts = await MyTischtennis.findAll({ + where: { + autoUpdateRatings: true, + savePassword: true // Must have saved password + }, + attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie'] + }); + + devLog(`Found ${accounts.length} accounts with auto-updates enabled`); + + if (accounts.length === 0) { + devLog('No accounts found with auto-updates enabled'); + return; + } + + // Process each account + for (const account of accounts) { + await this.processAccount(account); + } + + devLog('Automatic rating updates completed'); + } catch (error) { + console.error('Error in automatic rating updates:', error); + } + } + + /** + * Process a single account for rating updates + */ + async processAccount(account) { + const startTime = Date.now(); + let success = false; + let message = ''; + let errorDetails = null; + let updatedCount = 0; + + try { + devLog(`Processing account ${account.email} (User ID: ${account.userId})`); + + // Check if session is still valid + if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) { + devLog(`Session expired for ${account.email}, attempting re-login`); + + // Try to re-login with stored password + const password = account.getPassword(); + if (!password) { + throw new Error('No stored password available for re-login'); + } + + const loginResult = await myTischtennisClient.login(account.email, password); + if (!loginResult.success) { + throw new Error(`Re-login failed: ${loginResult.error}`); + } + + // Update session data + account.accessToken = loginResult.accessToken; + account.refreshToken = loginResult.refreshToken; + account.expiresAt = loginResult.expiresAt; + account.cookie = loginResult.cookie; + await account.save(); + + devLog(`Successfully re-logged in for ${account.email}`); + } + + // Perform rating update + const updateResult = await this.updateRatings(account); + updatedCount = updateResult.updatedCount || 0; + + success = true; + message = `Successfully updated ${updatedCount} ratings`; + devLog(`Updated ${updatedCount} ratings for ${account.email}`); + + } catch (error) { + success = false; + message = 'Update failed'; + errorDetails = error.message; + console.error(`Error updating ratings for ${account.email}:`, error); + } + + const executionTime = Date.now() - startTime; + + // Log the attempt + await myTischtennisService.logUpdateAttempt( + account.userId, + success, + message, + errorDetails, + updatedCount, + executionTime + ); + } + + /** + * Update ratings for a specific account + */ + async updateRatings(account) { + // TODO: Implement actual rating update logic + // This would typically involve: + // 1. Fetching current ratings from myTischtennis + // 2. Comparing with local data + // 3. Updating local member ratings + + devLog(`Updating ratings for ${account.email}`); + + // For now, simulate an update + await new Promise(resolve => setTimeout(resolve, 1000)); + + return { + success: true, + updatedCount: Math.floor(Math.random() * 10) // Simulate some updates + }; + } + + /** + * Get all accounts with auto-updates enabled (for manual execution) + */ + async getAutoUpdateAccounts() { + return await MyTischtennis.findAll({ + where: { + autoUpdateRatings: true + }, + attributes: ['userId', 'email', 'autoUpdateRatings', 'lastUpdateRatings'] + }); + } +} + +export default new AutoUpdateRatingsService(); diff --git a/backend/services/myTischtennisService.js b/backend/services/myTischtennisService.js index 121a653..66094c2 100644 --- a/backend/services/myTischtennisService.js +++ b/backend/services/myTischtennisService.js @@ -1,4 +1,5 @@ import MyTischtennis from '../models/MyTischtennis.js'; +import MyTischtennisUpdateHistory from '../models/MyTischtennisUpdateHistory.js'; import User from '../models/User.js'; import myTischtennisClient from '../clients/myTischtennisClient.js'; import HttpError from '../exceptions/HttpError.js'; @@ -11,7 +12,7 @@ class MyTischtennisService { async getAccount(userId) { const account = await MyTischtennis.findOne({ where: { userId }, - attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt'] + attributes: ['id', 'email', 'savePassword', 'autoUpdateRatings', 'lastLoginAttempt', 'lastLoginSuccess', 'lastUpdateRatings', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt'] }); return account; } @@ -19,7 +20,7 @@ class MyTischtennisService { /** * Create or update myTischtennis account */ - async upsertAccount(userId, email, password, savePassword, userPassword) { + async upsertAccount(userId, email, password, savePassword, autoUpdateRatings, userPassword) { // Verify user's app password const user = await User.findByPk(userId); if (!user) { @@ -51,6 +52,7 @@ class MyTischtennisService { // Update existing account.email = email; account.savePassword = savePassword; + account.autoUpdateRatings = autoUpdateRatings; if (password && savePassword) { account.setPassword(password); @@ -88,6 +90,7 @@ class MyTischtennisService { userId, email, savePassword, + autoUpdateRatings, lastLoginAttempt: password ? now : null, lastLoginSuccess: loginResult?.success ? now : null }; @@ -119,8 +122,10 @@ class MyTischtennisService { id: account.id, email: account.email, savePassword: account.savePassword, + autoUpdateRatings: account.autoUpdateRatings, lastLoginAttempt: account.lastLoginAttempt, lastLoginSuccess: account.lastLoginSuccess, + lastUpdateRatings: account.lastUpdateRatings, expiresAt: account.expiresAt }; } @@ -235,6 +240,53 @@ class MyTischtennisService { userData: account.userData }; } + + /** + * Get update ratings history for user + */ + async getUpdateHistory(userId) { + const history = await MyTischtennisUpdateHistory.findAll({ + where: { userId }, + order: [['createdAt', 'DESC']], + limit: 50 // Letzte 50 Einträge + }); + + return history.map(entry => ({ + id: entry.id, + success: entry.success, + message: entry.message, + errorDetails: entry.errorDetails, + updatedCount: entry.updatedCount, + executionTime: entry.executionTime, + createdAt: entry.createdAt + })); + } + + /** + * Log update ratings attempt + */ + async logUpdateAttempt(userId, success, message, errorDetails = null, updatedCount = 0, executionTime = null) { + try { + await MyTischtennisUpdateHistory.create({ + userId, + success, + message, + errorDetails, + updatedCount, + executionTime + }); + + // Update lastUpdateRatings in main table + if (success) { + await MyTischtennis.update( + { lastUpdateRatings: new Date() }, + { where: { userId } } + ); + } + } catch (error) { + console.error('Error logging update attempt:', error); + } + } } export default new MyTischtennisService(); diff --git a/backend/services/schedulerService.js b/backend/services/schedulerService.js new file mode 100644 index 0000000..eb0ca0f --- /dev/null +++ b/backend/services/schedulerService.js @@ -0,0 +1,108 @@ +import cron from 'node-cron'; +import autoUpdateRatingsService from './autoUpdateRatingsService.js'; +import { devLog } from '../utils/logger.js'; + +class SchedulerService { + constructor() { + this.jobs = new Map(); + this.isRunning = false; + } + + /** + * Start the scheduler + */ + start() { + if (this.isRunning) { + devLog('Scheduler is already running'); + return; + } + + devLog('Starting scheduler service...'); + + // Schedule automatic rating updates at 6:00 AM daily + const ratingUpdateJob = cron.schedule('0 6 * * *', async () => { + devLog('Executing scheduled rating updates...'); + try { + await autoUpdateRatingsService.executeAutomaticUpdates(); + } catch (error) { + console.error('Error in scheduled rating updates:', error); + } + }, { + scheduled: false, // Don't start automatically + timezone: 'Europe/Berlin' + }); + + this.jobs.set('ratingUpdates', ratingUpdateJob); + ratingUpdateJob.start(); + + this.isRunning = true; + devLog('Scheduler service started successfully'); + devLog('Rating updates scheduled for 6:00 AM daily (Europe/Berlin timezone)'); + } + + /** + * Stop the scheduler + */ + stop() { + if (!this.isRunning) { + devLog('Scheduler is not running'); + return; + } + + devLog('Stopping scheduler service...'); + + for (const [name, job] of this.jobs) { + job.stop(); + devLog(`Stopped job: ${name}`); + } + + this.jobs.clear(); + this.isRunning = false; + devLog('Scheduler service stopped'); + } + + /** + * Get scheduler status + */ + getStatus() { + return { + isRunning: this.isRunning, + jobs: Array.from(this.jobs.keys()), + timezone: 'Europe/Berlin' + }; + } + + /** + * Manually trigger rating updates (for testing) + */ + async triggerRatingUpdates() { + devLog('Manually triggering rating updates...'); + try { + await autoUpdateRatingsService.executeAutomaticUpdates(); + return { success: true, message: 'Rating updates completed successfully' }; + } catch (error) { + console.error('Error in manual rating updates:', error); + return { success: false, message: error.message }; + } + } + + /** + * Get next scheduled execution time for rating updates + */ + getNextRatingUpdateTime() { + const job = this.jobs.get('ratingUpdates'); + if (!job || !this.isRunning) { + return null; + } + + // Get next execution time (this is a simplified approach) + const now = new Date(); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(6, 0, 0, 0); + + return tomorrow; + } +} + +export default new SchedulerService(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 042b7c8..00a5bc4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "html2canvas": "^1.4.1", "jspdf": "^2.5.2", "jspdf-autotable": "^5.0.2", + "node-cron": "^4.2.1", "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", @@ -2545,6 +2546,15 @@ "license": "MIT", "optional": true }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b2cf85f..579f327 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "html2canvas": "^1.4.1", "jspdf": "^2.5.2", "jspdf-autotable": "^5.0.2", + "node-cron": "^4.2.1", "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", diff --git a/frontend/src/components/MyTischtennisDialog.vue b/frontend/src/components/MyTischtennisDialog.vue index cdd53c7..1eba6f9 100644 --- a/frontend/src/components/MyTischtennisDialog.vue +++ b/frontend/src/components/MyTischtennisDialog.vue @@ -41,6 +41,24 @@

+
+ +

+ Täglich um 6:00 Uhr werden automatisch die neuesten Ratings von myTischtennis abgerufen. + Erfordert gespeichertes Passwort. +

+

+ ⚠️ Für automatische Updates muss das myTischtennis-Passwort gespeichert werden. +

+
+
+ + + + + + diff --git a/frontend/src/views/MyTischtennisAccount.vue b/frontend/src/views/MyTischtennisAccount.vue index 26f269f..7c7fdc6 100644 --- a/frontend/src/views/MyTischtennisAccount.vue +++ b/frontend/src/views/MyTischtennisAccount.vue @@ -34,9 +34,20 @@ {{ formatDate(account.lastLoginAttempt) }}
+
+ + {{ formatDate(account.lastUpdateRatings) }} +
+ +
+ + {{ account.autoUpdateRatings ? 'Aktiviert' : 'Deaktiviert' }} +
+
+
@@ -66,6 +77,12 @@ @close="closeDialog" @saved="onAccountSaved" /> + + + @@ -93,16 +110,18 @@