diff --git a/backend/package.json b/backend/package.json index 3addc9aa6..f0daf7bd3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -89,19 +89,21 @@ "metascraper-video": "^5.7.4", "metascraper-youtube": "^5.7.4", "minimatch": "^3.0.4", + "mustache": "^3.0.3", "neo4j-driver": "~1.7.6", "neo4j-graphql-js": "^2.7.2", "neode": "^0.3.3", "node-fetch": "~2.6.0", "nodemailer": "^6.3.0", + "nodemailer-html-to-text": "^3.1.0", "npm-run-all": "~4.1.5", "request": "~2.88.0", "sanitize-html": "~1.20.1", "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.3", - "xregexp": "^4.2.4", - "wait-on": "~3.3.0" + "wait-on": "~3.3.0", + "xregexp": "^4.2.4" }, "devDependencies": { "@babel/cli": "~7.6.0", @@ -131,4 +133,4 @@ "prettier": "~1.18.2", "supertest": "~4.0.2" } -} \ No newline at end of file +} diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js index 809ca4072..e5b0678b8 100644 --- a/backend/src/middleware/email/emailMiddleware.js +++ b/backend/src/middleware/email/emailMiddleware.js @@ -1,36 +1,44 @@ import CONFIG from '../../config' import nodemailer from 'nodemailer' -import { resetPasswordMail, wrongAccountMail } from './templates/passwordReset' -import { signupTemplate } from './templates/signup' +import { htmlToText } from 'nodemailer-html-to-text' +import { + signupTemplate, + resetPasswordTemplate, + wrongAccountTemplate, +} from './templates/templateBuilder' -let sendMail -if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) { - sendMail = async templateArgs => { - await transporter().sendMail({ - from: '"Human Connection" ', - ...templateArgs, - }) - } -} else { - sendMail = () => {} +const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT +const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD + +let sendMail = () => {} +if (!hasEmailConfig) { if (process.env.NODE_ENV !== 'test') { // eslint-disable-next-line no-console console.log('Warning: Email middleware will not try to send mails.') } -} +} else { + sendMail = async templateArgs => { + const transporter = nodemailer.createTransport({ + host: CONFIG.SMTP_HOST, + port: CONFIG.SMTP_PORT, + ignoreTLS: CONFIG.SMTP_IGNORE_TLS, + secure: false, // true for 465, false for other ports + auth: hasAuthData && { + user: CONFIG.SMTP_USERNAME, + pass: CONFIG.SMTP_PASSWORD, + }, + }) -const transporter = () => { - const configs = { - host: CONFIG.SMTP_HOST, - port: CONFIG.SMTP_PORT, - ignoreTLS: CONFIG.SMTP_IGNORE_TLS, - secure: false, // true for 465, false for other ports + transporter.use( + 'compile', + htmlToText({ + ignoreImage: true, + wordwrap: false, + }), + ) + + await transporter.sendMail(templateArgs) } - const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG - if (user && pass) { - configs.auth = { user, pass } - } - return nodemailer.createTransport(configs) } const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { @@ -41,15 +49,17 @@ const sendSignupMail = async (resolve, root, args, context, resolveInfo) => { return response } +const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo) => { + const { email } = args + const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo) + const template = userFound ? resetPasswordTemplate : wrongAccountTemplate + await sendMail(template({ email, nonce, name })) + return true +} + export default { Mutation: { - requestPasswordReset: async (resolve, root, args, context, resolveInfo) => { - const { email } = args - const { email: emailFound, nonce, name } = await resolve(root, args, context, resolveInfo) - const mailTemplate = emailFound ? resetPasswordMail : wrongAccountMail - await sendMail(mailTemplate({ email, nonce, name })) - return true - }, + requestPasswordReset: sendPasswordResetMail, Signup: sendSignupMail, SignupByInvitation: sendSignupMail, }, diff --git a/backend/src/middleware/email/templates/passwordReset.js b/backend/src/middleware/email/templates/passwordReset.js deleted file mode 100644 index c977594b5..000000000 --- a/backend/src/middleware/email/templates/passwordReset.js +++ /dev/null @@ -1,83 +0,0 @@ -import CONFIG from '../../../config' - -export const resetPasswordMail = options => { - const { - name, - email, - nonce, - subject = 'Use this link to reset your password. The link is only valid for 24 hours.', - supportUrl = 'https://human-connection.org/en/contact/', - } = options - const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('nonce', nonce) - actionUrl.searchParams.set('email', email) - - return { - to: email, - subject, - text: ` -Hi ${name}! - -You recently requested to reset your password for your Human Connection account. -Use the link below to reset it. This password reset is only valid for the next -24 hours. - -${actionUrl} - -If you did not request a password reset, please ignore this email or contact -support if you have questions: - -${supportUrl} - -Thanks, -The Human Connection Team - -If you're having trouble with the link above, you can manually copy and -paste the following code into your browser window: - -${nonce} - -Human Connection gemeinnützige GmbH -Bahnhofstr. 11 -73235 Weilheim / Teck -Deutschland - `, - } -} - -export const wrongAccountMail = options => { - const { - email, - subject = `We received a request to reset your password with this email address (${email})`, - supportUrl = 'https://human-connection.org/en/contact/', - } = options - const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) - return { - to: email, - subject, - text: ` -We received a request to reset the password to access Human Connection with your -email address, but we were unable to find an account associated with this -address. - -If you use Human Connection and were expecting this email, consider trying to -request a password reset using the email address associated with your account. -Try a different email: - -${actionUrl} - -If you do not use Human Connection or did not request a password reset, please -ignore this email. Feel free to contact support if you have further questions: - -${supportUrl} - -Thanks, -The Human Connection Team - -Human Connection gemeinnützige GmbH -Bahnhofstr. 11 -73235 Weilheim / Teck -Deutschland - `, - } -} diff --git a/backend/src/middleware/email/templates/resetPassword.html b/backend/src/middleware/email/templates/resetPassword.html new file mode 100644 index 000000000..e0dde53e5 --- /dev/null +++ b/backend/src/middleware/email/templates/resetPassword.html @@ -0,0 +1,448 @@ + + + + + + + + + + Neues Passwort | Reset Password + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + diff --git a/backend/src/middleware/email/templates/signup.html b/backend/src/middleware/email/templates/signup.html new file mode 100644 index 000000000..e4be8c02f --- /dev/null +++ b/backend/src/middleware/email/templates/signup.html @@ -0,0 +1,485 @@ + + + + + + + + + + Willkommen, Bienvenue, Welcome to Human Connection + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ Dein Anmeldelink. | Here is your signup link. +
+
+ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ 
+
+ + + + +
+ + + diff --git a/backend/src/middleware/email/templates/signup.js b/backend/src/middleware/email/templates/signup.js deleted file mode 100644 index 54cc51be2..000000000 --- a/backend/src/middleware/email/templates/signup.js +++ /dev/null @@ -1,62 +0,0 @@ -import CONFIG from '../../../config' - -export const signupTemplate = options => { - const { - email, - nonce, - subject = 'Welcome to Human Connection! Here is your signup link.', - supportUrl = 'https://human-connection.org/en/contact/', - } = options - const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('nonce', nonce) - actionUrl.searchParams.set('email', email) - - return { - to: email, - subject, - text: ` -Willkommen bei Human Connection! Klick auf diesen Link, um den -Registrierungsprozess abzuschließen und um ein Benutzerkonto zu erstellen! - -${actionUrl} - -Alternativ kannst du diesen Code auch kopieren und im Browserfenster einfügen: - -${nonce} - -Bitte ignoriere diese Mail, falls du dich nicht bei Human Connection angemeldet -hast. Bei Fragen kontaktiere gerne unseren Support: - -${supportUrl} - -Danke, -Das Human Connection Team - - -English Version -=============== - -Welcome to Human Connection! Use this link to complete the registration process -and create a user account: - -${actionUrl} - -You can also copy+paste this verification nonce in your browser window: - -${nonce} - -If you did not signed up for Human Connection, please ignore this email or -contact support if you have questions: - -${supportUrl} - -Thanks, -The Human Connection Team - -Human Connection gemeinnützige GmbH -Bahnhofstr. 11 -73235 Weilheim / Teck -Deutschland - `, - } -} diff --git a/backend/src/middleware/email/templates/templateBuilder.js b/backend/src/middleware/email/templates/templateBuilder.js new file mode 100644 index 000000000..fdeb47a89 --- /dev/null +++ b/backend/src/middleware/email/templates/templateBuilder.js @@ -0,0 +1,48 @@ +import fs from 'fs' +import path from 'path' +import mustache from 'mustache' +import CONFIG from '../../../config' + +const from = '"Human Connection" ' +const supportUrl = 'https://human-connection.org/en/contact' + +const signupHtml = fs.readFileSync(path.join(__dirname, './signup.html'), 'utf-8') +const passwordResetHtml = fs.readFileSync(path.join(__dirname, './resetPassword.html'), 'utf-8') +const wrongAccountHtml = fs.readFileSync(path.join(__dirname, './wrongAccount.html'), 'utf-8') + +export const signupTemplate = ({ email, nonce }) => { + const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('nonce', nonce) + actionUrl.searchParams.set('email', email) + + return { + from, + to: email, + subject: 'Willkommen, Bienvenue, Welcome to Human Connection!', + html: mustache.render(signupHtml, { actionUrl, supportUrl }), + } +} + +export const resetPasswordTemplate = ({ email, nonce, name }) => { + const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) + actionUrl.searchParams.set('nonce', nonce) + actionUrl.searchParams.set('email', email) + + return { + from, + to: email, + subject: 'Neues Passwort | Reset Password', + html: mustache.render(passwordResetHtml, { actionUrl, name, nonce, supportUrl }), + } +} + +export const wrongAccountTemplate = ({ email }) => { + const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) + + return { + from, + to: email, + subject: 'Falsche Mailadresse? | Wrong E-mail?', + html: mustache.render(wrongAccountHtml, { actionUrl, supportUrl }), + } +} diff --git a/backend/src/middleware/email/templates/wrongAccount.html b/backend/src/middleware/email/templates/wrongAccount.html new file mode 100644 index 000000000..b8e6f6f57 --- /dev/null +++ b/backend/src/middleware/email/templates/wrongAccount.html @@ -0,0 +1,448 @@ + + + + + + + + + + Falsche Mailadresse? | Wrong E-mail? + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + diff --git a/backend/src/schema/resolvers/passwordReset/emailTemplates.js b/backend/src/schema/resolvers/passwordReset/emailTemplates.js deleted file mode 100644 index 8508adccc..000000000 --- a/backend/src/schema/resolvers/passwordReset/emailTemplates.js +++ /dev/null @@ -1,85 +0,0 @@ -import CONFIG from '../../../config' - -export const from = '"Human Connection" ' - -export const resetPasswordMail = options => { - const { - name, - email, - code, - subject = 'Use this link to reset your password. The link is only valid for 24 hours.', - supportUrl = 'https://human-connection.org/en/contact/', - } = options - const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI) - actionUrl.searchParams.set('code', code) - actionUrl.searchParams.set('email', email) - - return { - to: email, - subject, - text: ` -Hi ${name}! - -You recently requested to reset your password for your Human Connection account. -Use the link below to reset it. This password reset is only valid for the next -24 hours. - -${actionUrl} - -If you did not request a password reset, please ignore this email or contact -support if you have questions: - -${supportUrl} - -Thanks, -The Human Connection Team - -If you're having trouble with the link above, you can manually copy and -paste the following code into your browser window: - -${code} - -Human Connection gemeinnützige GmbH -Bahnhofstr. 11 -73235 Weilheim / Teck -Deutschland - `, - } -} - -export const wrongAccountMail = options => { - const { - email, - subject = `We received a request to reset your password with this email address (${email})`, - supportUrl = 'https://human-connection.org/en/contact/', - } = options - const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI) - return { - to: email, - subject, - text: ` -We received a request to reset the password to access Human Connection with your -email address, but we were unable to find an account associated with this -address. - -If you use Human Connection and were expecting this email, consider trying to -request a password reset using the email address associated with your account. -Try a different email: - -${actionUrl} - -If you do not use Human Connection or did not request a password reset, please -ignore this email. Feel free to contact support if you have further questions: - -${supportUrl} - -Thanks, -The Human Connection Team - -Human Connection gemeinnützige GmbH -Bahnhofstr. 11 -73235 Weilheim / Teck -Deutschland - `, - } -} diff --git a/backend/yarn.lock b/backend/yarn.lock index e380c6ed9..85e64d9ec 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -4323,6 +4323,11 @@ he@0.5.0: resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + helmet-crossdomain@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e" @@ -4395,7 +4400,17 @@ html-encoding-sniffer@^1.0.2: dependencies: whatwg-encoding "^1.0.1" -htmlparser2@^3.10.0, htmlparser2@^3.9.1: +html-to-text@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-5.1.1.tgz#2d89db7bf34bc7bcb7d546b1b228991a16926e87" + integrity sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA== + dependencies: + he "^1.2.0" + htmlparser2 "^3.10.1" + lodash "^4.17.11" + minimist "^1.2.0" + +htmlparser2@^3.10.0, htmlparser2@^3.10.1, htmlparser2@^3.9.1: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -6134,6 +6149,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mustache@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-3.0.3.tgz#ee4fb971887fa6cc1b6b6d219a74b5e3c7535f32" + integrity sha512-vM5FkMHamTYmVYeAujypihuPrJQDtaUIlKeeVb1AMJ73OZLtWiF7GprqrjxD0gJWT53W9JfqXxf97nXQjMQkqA== + mute-stream@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -6324,6 +6344,13 @@ node-releases@^1.1.25: dependencies: semver "^5.3.0" +nodemailer-html-to-text@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nodemailer-html-to-text/-/nodemailer-html-to-text-3.1.0.tgz#11e4e435eb03e4f3b439aaf294b1bd1377e7f789" + integrity sha512-AijyAZgcFb6b53g1oMwdCKyLYQVJzbgZKbs3Bma8zR5hPR1gkajQKGGZbwtuA5JhUqnyC8pjp+tiaS7CkQ8TRg== + dependencies: + html-to-text "^5.1.1" + nodemailer@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12"