From cb5f4b6ef4446520aca4d3ffa0002584a31c8c3d Mon Sep 17 00:00:00 2001 From: HF Date: Mon, 6 Jan 2020 11:46:11 +0100 Subject: [PATCH] change from sendmail to nodemailer fix eslint errors corresponding to mail sending --- README.md | 21 ++-- package-lock.json | 121 +-------------------- package.json | 2 +- src/core/mail.js | 137 +++++++++++++++--------- src/routes/api/auth/change_mail.js | 2 +- src/routes/api/auth/register.js | 2 +- src/routes/api/auth/resend_verify.js | 2 +- src/routes/api/auth/restore_password.js | 2 +- src/routes/reset_password.js | 6 +- 9 files changed, 111 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index 7c434dd..b01097f 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,11 @@ npm run build All needed files to run it got created in `./build` -#### Note: -If you run into problems, make sure that you have rights to g++ (if not, run as root and then chown username:username -R . after build) +Notes: -If `npm install` fails with "unable to connect to github.com" set: +- If you run into problems, make sure that you have rights to g++ (if not, run as root and then chown username:username -R . after build) + +- If `npm install` fails with "unable to connect to github.com" set: ``` git config --global url.https://github.com/.insteadOf git://github.com/ @@ -54,7 +55,7 @@ git config --global url.https://github.com/.insteadOf git://github.com/ ### Requirements - nodejs environment with [npm](https://www.npmjs.com/get-npm) - [pm2](https://github.com/Unitech/pm2) (`npm install -g pm2`) as process manager and for logging -- [redis](https://redis.io/) as database for storgìng the canvas +- [redis](https://redis.io/) as database for storìng the canvas - mysql or mariadb ([setup own user](https://www.digitalocean.com/community/tutorials/how-to-create-a-new-user-and-grant-permissions-in-mysql) and [create database](https://www.w3schools.com/SQl/sql_create_db.asp) for pixelplanet) for storing additional data like IP blacklist ### Configuration @@ -108,7 +109,7 @@ Notes: | REDDIT_CLIENT_ID | Media | | REDDIT_CLIENT_SECRET | Accounts | -Note: +Notes: - The HTML for SocialMedia logins is in src/componets/UserAreaModal.js , delete stuff from there if you don't need it - The HTML for the Help Screen is in src/components/HelpModal.js @@ -124,20 +125,28 @@ The default configuration values can be seen in `src/core/config.js` and for the 1. Make sure that mysql and redis are running 3. Start with + ``` pm2 start ecosystem.yml ``` -Note: It might be neccessary to change the charset and collate of the sql colum names of table Users to support special character names, which can be done with the SQL command: + +Notes: + +- pixelplanet uses the unix command sendmail for sending verification and password reset mails. If you are on windows, this might not work. +- It might be neccessary to change the charset and collate of the sql colum names of table Users to support special character names, which can be done with the SQL command: ``` ALTER TABLE Users CONVERT TO CHARACTER SET utf8mb4 COLLATE 'utf8mb4_unicode_ci'; ``` ### Logging logs are in ~/pm2/log/, you can view them with + ``` pm2 log web ``` + you can flush the logs with + ``` pm2 log flush ``` diff --git a/package-lock.json b/package-lock.json index ec2bbfa..3aafe6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1543,11 +1543,6 @@ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, - "addressparser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", - "integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y=" - }, "agent-base": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", @@ -2586,36 +2581,6 @@ "node-gyp-build": "~3.7.0" } }, - "buildmail": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-3.10.0.tgz", - "integrity": "sha1-xoJtcW55RbtvaxQ0tTmF4CmgMVk=", - "requires": { - "addressparser": "1.0.1", - "libbase64": "0.1.0", - "libmime": "2.1.0", - "libqp": "1.1.0", - "nodemailer-fetch": "1.6.0", - "nodemailer-shared": "1.1.0" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", - "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" - }, - "libmime": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.1.0.tgz", - "integrity": "sha1-Ubx23iKDFh65BRxLyArtcT5P0c0=", - "requires": { - "iconv-lite": "0.4.13", - "libbase64": "0.1.0", - "libqp": "1.1.0" - } - } - } - }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -3996,14 +3961,6 @@ "randombytes": "^2.0.0" } }, - "dkim-signer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dkim-signer/-/dkim-signer-0.2.2.tgz", - "integrity": "sha1-qoHsBx7u02IngbqpIgRNeADl8wg=", - "requires": { - "libmime": "^2.0.3" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -7251,33 +7208,6 @@ "type-check": "~0.3.2" } }, - "libbase64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", - "integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY=" - }, - "libmime": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.1.3.tgz", - "integrity": "sha1-JQF8pataHpiq2+JyUBfPHUikKgw=", - "requires": { - "iconv-lite": "0.4.15", - "libbase64": "0.1.0", - "libqp": "1.1.0" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" - } - } - }, - "libqp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", - "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=" - }, "lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -7482,32 +7412,6 @@ } } }, - "mailcomposer": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.12.0.tgz", - "integrity": "sha1-nF4RiKqOHGLsi4a9Q0aBArY56Pk=", - "requires": { - "buildmail": "3.10.0", - "libmime": "2.1.0" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", - "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" - }, - "libmime": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.1.0.tgz", - "integrity": "sha1-Ubx23iKDFh65BRxLyArtcT5P0c0=", - "requires": { - "iconv-lite": "0.4.13", - "libbase64": "0.1.0", - "libqp": "1.1.0" - } - } - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -8201,18 +8105,10 @@ "true-case-path": "^1.0.2" } }, - "nodemailer-fetch": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz", - "integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q=" - }, - "nodemailer-shared": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz", - "integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=", - "requires": { - "nodemailer-fetch": "1.6.0" - } + "nodemailer": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.2.tgz", + "integrity": "sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ==" }, "noop-logger": { "version": "0.1.1", @@ -10158,15 +10054,6 @@ } } }, - "sendmail": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sendmail/-/sendmail-1.6.1.tgz", - "integrity": "sha512-lIhvnjSi5e5jL8wA1GPP6j2QVlx6JOEfmdn0QIfmuJdmXYGmJ375kcOU0NSm/34J+nypm4sa1AXrYE5w3uNIIA==", - "requires": { - "dkim-signer": "0.2.2", - "mailcomposer": "3.12.0" - } - }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", diff --git a/package.json b/package.json index 82b5fa6..388bc3c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "multer": "^1.4.1", "mysql2": "^2.1.0", "node-sass": "^4.11.0", + "nodemailer": "^6.4.2", "passport": "^0.4.0", "passport-discord": "^0.1.2", "passport-facebook": "^3.0.0", @@ -72,7 +73,6 @@ "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", "redux-thunk": "^2.2.0", - "sendmail": "^1.6.1", "sequelize": "^5.19.2", "sharp": "^0.23.4", "startaudiocontext": "^1.2.1", diff --git a/src/core/mail.js b/src/core/mail.js index d8bc62b..6079399 100644 --- a/src/core/mail.js +++ b/src/core/mail.js @@ -3,50 +3,66 @@ * @flow */ -// must use require for arguments import Sequelize from 'sequelize'; -import logger from './logger'; +import nodemailer from 'nodemailer'; +import logger from './logger'; import { HOUR, MINUTE } from './constants'; import { HOSTURL } from './config'; import { DailyCron, HourlyCron } from '../utils/cron'; import RegUser from '../data/models/RegUser'; -const sendmail = require('sendmail')({ silent: true }); + +/* + * define mail transport + * using unix command sendmail + */ +const transporter = nodemailer.createTransport({ + sendmail: true, + newline: 'unix', + path: '/usr/sbin/sendmail', +}); // TODO make code expire class MailProvider { - verify_codes: Object; + verifyCodes: Object; constructor() { - this.clear_codes = this.clear_codes.bind(this); + this.clearCodes = this.clearCodes.bind(this); - this.verify_codes = {}; - HourlyCron.hook(this.clear_codes); - DailyCron.hook(MailProvider.clean_users); + this.verifyCodes = {}; + HourlyCron.hook(this.clearCodes); + DailyCron.hook(MailProvider.cleanUsers); } - send_verify_mail(to, name) { - const past_mail = this.verify_codes[to]; - if (past_mail) { - const min_left = Math.floor(past_mail.timestamp / MINUTE + 15 - Date.now() / MINUTE); - if (min_left > 0) { - logger.info(`Verify mail for ${to} - already sent, ${min_left} minutes left`); - return `We already sent you a verification mail, you can request another one in ${min_left} minutes.`; + sendVerifyMail(to, name) { + const pastMail = this.verifyCodes[to]; + if (pastMail) { + const minLeft = Math.floor( + pastMail.timestamp / MINUTE + 15 - Date.now() / MINUTE, + ); + if (minLeft > 0) { + logger.info( + `Verify mail for ${to} - already sent, ${minLeft} minutes left`, + ); + // eslint-disable-next-line max-len + return `We already sent you a verification mail, you can request another one in ${minLeft} minutes.`; } } logger.info(`Sending verification mail to ${to} / ${name}`); - const code = this.set_code(to); - const verify_url = `${HOSTURL}/api/auth/verify?token=${code}`; - sendmail({ + const code = this.setCode(to); + const verifyUrl = `${HOSTURL}/api/auth/verify?token=${code}`; + transporter.sendMail({ from: 'donotreply@pixelplanet.fun', to, replyTo: 'donotreply@pixelplanet.fun', + // eslint-disable-next-line max-len subject: `Welcome ${name} to PixelPlanet, plese verify your mail`, - text: `Hello,\nwelcome to our little community of pixelplacers, to use your account, you have to verify your mail. You can do that here:\n ${verify_url} \nHave fun and don't hesitate to contact us if you encouter any problems :)\nThanks`, - }, (err, reply) => { + // eslint-disable-next-line max-len + text: `Hello,\nwelcome to our little community of pixelplacers, to use your account, you have to verify your mail. You can do that here:\n ${verifyUrl} \nHave fun and don't hesitate to contact us if you encouter any problems :)\nThanks`, + }, (err) => { if (err) { logger.error(err & err.stack); } @@ -54,17 +70,22 @@ class MailProvider { return null; } - async send_passd_reset_mail(to, ip) { - const past_mail = this.verify_codes[to]; - if (past_mail) { - if (Date.now() < past_mail.timestamp + 15 * MINUTE) { - logger.info(`Password reset mail for ${to} requested by ${ip} - already sent`); + async sendPasswdResetMail(to, ip) { + const pastMail = this.verifyCodes[to]; + if (pastMail) { + if (Date.now() < pastMail.timestamp + 15 * MINUTE) { + logger.info( + `Password reset mail for ${to} requested by ${ip} - already sent`, + ); + // eslint-disable-next-line max-len return 'We already sent you a mail with instructions. Please wait before requesting another mail.'; } } const reguser = await RegUser.findOne({ where: { email: to } }); - if (past_mail || !reguser) { - logger.info(`Password reset mail for ${to} requested by ${ip} - mail not found`); + if (pastMail || !reguser) { + logger.info( + `Password reset mail for ${to} requested by ${ip} - mail not found`, + ); return "Couldn't find this mail in our database"; } /* @@ -78,15 +99,17 @@ class MailProvider { */ logger.info(`Sending Password reset mail to ${to}`); - const code = this.set_code(to); - const restore_url = `${HOSTURL}/reset_password?token=${code}`; - sendmail({ + const code = this.setCode(to); + const restoreUrl = `${HOSTURL}/reset_password?token=${code}`; + transporter.sendMail({ from: 'donotreply@pixelplanet.fun', to, replyTo: 'donotreply@pixelplanet.fun', + // eslint-disable-next-line max-len subject: 'You forgot your password for PixelPlanet? Get a new one here', - text: `Hello,\nYou requested to get a new password. You can change your password within the next 30min here:\n ${restore_url} \nHave fun and don't hesitate to contact us if you encouter any problems :)\nIf you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).\nThanks`, - }, (err, reply) => { + // eslint-disable-next-line max-len + text: `Hello,\nYou requested to get a new password. You can change your password within the next 30min here:\n ${restoreUrl} \nHave fun and don't hesitate to contact us if you encouter any problems :)\nIf you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).\nThanks`, + }, (err) => { if (err) { logger.error(err & err.stack); } @@ -94,34 +117,39 @@ class MailProvider { return null; } - set_code(email) { - const code = MailProvider.create_code(); - this.verify_codes[email] = { + setCode(email) { + const code = MailProvider.createCode(); + this.verifyCodes[email] = { code, timestamp: Date.now(), }; return code; } - async clear_codes() { - const cur_time = Date.now(); - const to_delete = []; - for (const iteremail in this.verify_codes) { - if (cur_time > this.verify_codes[iteremail].timestamp + HOUR) { - to_delete.push(iteremail); + async clearCodes() { + const curTime = Date.now(); + const toDelete = []; + + const mails = Object.keys(this.verifyCodes); + for (let i = 0; i < mails.length; i += 1) { + const iteremail = mails[i]; + if (curTime > this.verifyCodes[iteremail].timestamp + HOUR) { + toDelete.push(iteremail); } } - to_delete.forEach((email) => { + toDelete.forEach((email) => { logger.info(`Mail Code for ${email} expired`); - delete this.verify_codes[email]; + delete this.verifyCodes[email]; }); } // Note: code gets deleted on check - check_code(code) { + checkCode(code) { let email = null; - for (const iteremail in this.verify_codes) { - if (this.verify_codes[iteremail].code == code) { + const mails = Object.keys(this.verifyCodes); + for (let i = 0; i < mails.length; i += 1) { + const iteremail = mails[i]; + if (this.verifyCodes[iteremail].code === code) { email = iteremail; break; } @@ -131,12 +159,12 @@ class MailProvider { return false; } logger.info(`Got Mail Code from ${email}.`); - delete this.verify_codes[email]; + delete this.verifyCodes[email]; return email; } async verify(code) { - const email = this.check_code(code); + const email = this.checkCode(code); if (!email) return false; const reguser = await RegUser.findOne({ where: { email } }); @@ -151,18 +179,21 @@ class MailProvider { return true; } - static create_code() { - const part1 = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - const part2 = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + static createCode() { + const part1 = Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + const part2 = Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); return `${part1}-${part2}`; } - static clean_users() { + static cleanUsers() { // delete users that requier verification for more than 4 days RegUser.destroy({ where: { verificationReqAt: { - [Sequelize.Op.lt]: Sequelize.literal('CURRENT_TIMESTAMP - INTERVAL 4 DAY'), + [Sequelize.Op.lt]: + Sequelize.literal('CURRENT_TIMESTAMP - INTERVAL 4 DAY'), }, // NOTE: this means that minecraft verified accounts do not get deleted verified: 0, diff --git a/src/routes/api/auth/change_mail.js b/src/routes/api/auth/change_mail.js index cea7866..cd4a7d3 100644 --- a/src/routes/api/auth/change_mail.js +++ b/src/routes/api/auth/change_mail.js @@ -55,7 +55,7 @@ export default async (req: Request, res: Response) => { mailVerified: false, }); - mailProvider.send_verify_mail(email, user.regUser.name); + mailProvider.sendVerifyMail(email, user.regUser.name); res.json({ success: true, diff --git a/src/routes/api/auth/register.js b/src/routes/api/auth/register.js index dbcff25..01a7ff5 100644 --- a/src/routes/api/auth/register.js +++ b/src/routes/api/auth/register.js @@ -70,7 +70,7 @@ export default async (req: Request, res: Response) => { }); return; } - mailProvider.send_verify_mail(email, name); + mailProvider.sendVerifyMail(email, name); res.status(200); res.json({ success: true, diff --git a/src/routes/api/auth/resend_verify.js b/src/routes/api/auth/resend_verify.js index ee488d2..3d7627b 100644 --- a/src/routes/api/auth/resend_verify.js +++ b/src/routes/api/auth/resend_verify.js @@ -26,7 +26,7 @@ export default async (req: Request, res: Response) => { return; } - const error = mailProvider.send_verify_mail(email, name); + const error = mailProvider.sendVerifyMail(email, name); if (error) { res.status(400); res.json({ diff --git a/src/routes/api/auth/restore_password.js b/src/routes/api/auth/restore_password.js index 421ddd6..8b8553b 100644 --- a/src/routes/api/auth/restore_password.js +++ b/src/routes/api/auth/restore_password.js @@ -28,7 +28,7 @@ export default async (req: Request, res: Response) => { }); return; } - const error = await mailProvider.send_passd_reset_mail(email, ip); + const error = await mailProvider.sendPasswdResetMail(email, ip); if (error) { res.status(400); res.json({ diff --git a/src/routes/reset_password.js b/src/routes/reset_password.js index b936224..eaf32f8 100644 --- a/src/routes/reset_password.js +++ b/src/routes/reset_password.js @@ -54,7 +54,7 @@ router.post('/', async (req: Request, res: Response, next) => { return; } - const email = mailProvider.check_code(code); + const email = mailProvider.checkCode(code); if (!email) { const html = getPasswordResetHtml(null, null, "This password-reset link isn't valid anymore :("); res.status(401).send(html); @@ -94,14 +94,14 @@ router.get('/', async (req: Request, res: Response, next) => { return; } - const email = mailProvider.check_code(token); + const email = mailProvider.checkCode(token); if (!email) { const html = getPasswordResetHtml(null, null, 'This passwort reset link is wrong or already expired, please request a new one (Note: you can use those links just once)'); res.status(401).send(html); return; } - const code = mailProvider.set_code(email); + const code = mailProvider.setCode(email); const html = getPasswordResetHtml(email, code); res.status(200).send(html); });