diff --git a/server/.env.example b/server/.env.example index d4c7a53bc202a6dd7a9d3f99af9fdbbca52b222b..aae109702f7113a466ce376c0401dc6ee936f822 100644 --- a/server/.env.example +++ b/server/.env.example @@ -5,3 +5,9 @@ DB_HOST=localhost DB_DRIVER=postgres DB_PASSWORD=postgres TEST_DB_NAME=test +EMAIL_SERVER=smtp.example.com +EMAIL_IS_TLS=false +EMAIL_USERNAME=username +EMAIL_PASSWORD=pw +EMAIL_FROM_ADDR=username@example.com +Application_Name=Fahrtenbuch Wassersportzentrum diff --git a/server/package-lock.json b/server/package-lock.json index 4d943da3a414817692b40a607fea05e1847130c5..042372310669776c484c01de6ce98dc677862334 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,8 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/nodemailer": "^6.4.4", "dotenv": "^10.0.0", "express": "^4.17.1", + "handlebars": "^4.7.7", + "nodemailer": "^6.7.2", "pg": "^8.7.1", "sequelize": "^6.9.0" }, @@ -1167,6 +1170,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" }, + "node_modules/@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz", @@ -2797,6 +2808,26 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4297,8 +4328,7 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/moment": { "version": "2.29.1", @@ -4338,6 +4368,11 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4359,6 +4394,14 @@ "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz", + "integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", @@ -5340,7 +5383,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5890,6 +5932,18 @@ "node": ">=4.2.0" } }, + "node_modules/uglify-js": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.4.tgz", + "integrity": "sha512-AbiSR44J0GoCeV81+oxcy/jDOElO2Bx3d0MfQCUShq7JRXaM4KtQopZsq2vFv8bCq2yMaGrw1FgygUd03RyRDA==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6137,6 +6191,11 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -7188,6 +7247,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==" }, + "@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "requires": { + "@types/node": "*" + } + }, "@types/prettier": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.2.tgz", @@ -8462,6 +8529,18 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -9609,8 +9688,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "moment": { "version": "2.29.1", @@ -9641,6 +9719,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9659,6 +9742,11 @@ "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, + "nodemailer": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz", + "integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==" + }, "nodemon": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", @@ -10394,8 +10482,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-support": { "version": "0.5.21", @@ -10775,6 +10862,12 @@ "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, + "uglify-js": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.4.tgz", + "integrity": "sha512-AbiSR44J0GoCeV81+oxcy/jDOElO2Bx3d0MfQCUShq7JRXaM4KtQopZsq2vFv8bCq2yMaGrw1FgygUd03RyRDA==", + "optional": true + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -10967,6 +11060,11 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/server/package.json b/server/package.json index 804afe8971ba8c003572af5e926c80e340865203..73ca2dc001f6c78ca3a9a5a921555f2a2617b5c0 100644 --- a/server/package.json +++ b/server/package.json @@ -12,8 +12,11 @@ "author": "", "license": "ISC", "dependencies": { + "@types/nodemailer": "^6.4.4", "dotenv": "^10.0.0", "express": "^4.17.1", + "handlebars": "^4.7.7", + "nodemailer": "^6.7.2", "pg": "^8.7.1", "sequelize": "^6.9.0" }, diff --git a/server/src/assets/notification.html b/server/src/assets/notification.html new file mode 100644 index 0000000000000000000000000000000000000000..d11553e5cd77bc07491f3212d73babf0c68684ba --- /dev/null +++ b/server/src/assets/notification.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html lang="en" style="background:#f5f5f5" xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> +<body style="margin: 0; padding: 0; font-family:Lato, sans-serif;"> +<table border="0" cellpadding="0" cellspacing="0" width="100%"> + <tr> + <td style="padding: 20px 0 30px 0;"> + <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"> + <tr> + <td bgcolor="#6b9e1f" height="65" style="color: white; font-family: Arial, sans-serif; font-size: 26px; padding: 10px;"> + {{application_name}} Benachrichtigung + </td> + </tr> + <tr> + <td bgcolor="#ffffff" style="padding: 10px;"> + <span>Es wurde eine Anmerkung zu <a style="letter-spacing: 1px; color: #37557D;" href="#">{{boat}}</a> verfasst:</span> + <table role="presentation" bgcolor="#EAF0F6" width="80%" align="center" style="margin-top: 10px"> + <tr> + <td align="left" style="padding: 30px 30px;"> + {{notification}} + </td> + </tr> + </table> + <table role="presentation" bgcolor="#37557D" style="color: white" width="80%" align="center"> + <tr> + <td align="left" style="padding: 5px 30px;"> + Author: {{author}} + </td> + <td align="right" style="padding: 5px 30px;"> + Time: {{timestamp}} + </td> + </tr> + </table> + </td> + </tr> + </table> + <!-- Manage Notifications --> + <table bgcolor="#F5F8FA" width="100%" > + <tr> + <td align="left" style="padding: 20px 30px;"> + <a style="font-size: 9px; text-transform:uppercase; letter-spacing: 1px; color: #CBD6E2;" href="{{manage_notification_link}}"> Manage Notifications </a> + </td> + </tr> + </table> + </td> + </tr> +</table> +</body> +</html> diff --git a/server/src/assets/notification.txt b/server/src/assets/notification.txt new file mode 100644 index 0000000000000000000000000000000000000000..743b819f78327117ce3732546bdbe0955f206e54 --- /dev/null +++ b/server/src/assets/notification.txt @@ -0,0 +1,4 @@ +{application_name}} Benachrichtigung +Es wurde eine Anmerkung zu {{boat}} verfasst: +{{notification}} +Author: {{author}} Time: {{timestamp}} diff --git a/server/src/mail.ts b/server/src/mail.ts new file mode 100644 index 0000000000000000000000000000000000000000..00ae81b4bab1bed6b59ad34a2ef42df9f2749f5e --- /dev/null +++ b/server/src/mail.ts @@ -0,0 +1,64 @@ +import nodemailer from "nodemailer"; +import path from 'path'; +import fs from 'fs'; +import Handlebars from 'handlebars'; + +let transporter: nodemailer.Transporter = undefined; +let ready = false; + + +let create = async () => { + transporter = nodemailer.createTransport({ + host: process.env.EMAIL_SERVER, + port: process.env.EMAIL_Port ? Number(process.env.EMAIL_Port) : (Boolean(process.env.EMAIL_IS_TLS) ? 465 : 587), + secure: Boolean(process.env.EMAIL_IS_TLS), + auth: { + user: process.env.EMAIL_USERNAME, + pass: process.env.EMAIL_PASSWORD + } + }); + transporter.verify(function (error, success) { + if (error) { + console.log("Error verifying Email connection: " + error); + process.exit(9) + } else { + ready = true; + } + }); +} +create(); + +// Construct Templates and load to RAM +let templating: { [templateName: string]: { html: HandlebarsTemplateDelegate, txt: HandlebarsTemplateDelegate } } = {}; +fs.readdirSync('./assets').forEach(file => { + if (file.includes('.html')) { + try { + templating[file.split('.')[0]] = { + html: Handlebars.compile(fs.readFileSync(path.join("./assets", file), 'utf8')), + txt: Handlebars.compile(fs.readFileSync(path.join("./assets", file.split('.')[0] + '.txt'), 'utf8')) + }; + } catch (e) { + console.log(`Couldn't load template '${file}'`, e) + } + } +}); + +const sendMail = (template: "notification", to: string, subject: string, replace: { [toReplace: string]: string }) => { + if (!ready || !templating[template]) { + console.log(ready, templating[template]) + return + } + transporter.sendMail({ + from: `"${process.env.Application_Name}" <${process.env.EMAIL_FROM_ADDR}>`, + to: to, + subject: subject, + text: templating[template].txt(replace), + html: templating[template].html(replace) + }, (err, info) => { + if (err) return console.log('error', JSON.stringify(err)); + //if (info) return logger.log('info', JSON.stringify(info)); + return null; + }); +} + +export default sendMail diff --git a/server/src/server.ts b/server/src/server.ts index bad4cd10df712d506cde9424d0090058295c7bf8..c4c15568312f6f170dd7fe97e2bcd55a23cac12b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2,6 +2,7 @@ import "dotenv/config"; import express from "express"; import dbInit from "./db/db_init"; import apiRouter from "./routes"; +import sendMail from "./mail"; let init = async () => { await dbInit();