[N8N-4355] Use safer templating for UserManagement emails (#3893)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-08-19 12:45:22 +02:00 committed by GitHub
parent dc8f8b7874
commit c65458c154
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 36 additions and 36 deletions

1
package-lock.json generated
View file

@ -53645,6 +53645,7 @@
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"flatted": "^3.2.4", "flatted": "^3.2.4",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"handlebars": "4.7.7",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"json-diff": "^0.5.4", "json-diff": "^0.5.4",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",

View file

@ -129,6 +129,7 @@
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"flatted": "^3.2.4", "flatted": "^3.2.4",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"handlebars": "4.7.7",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"json-diff": "^0.5.4", "json-diff": "^0.5.4",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",

View file

@ -1,7 +1,8 @@
/* eslint-disable import/no-cycle */ import Handlebars from 'handlebars';
import { existsSync, readFileSync } from 'fs'; import { existsSync } from 'fs';
import { IDataObject } from 'n8n-workflow'; import { readFile } from 'fs/promises';
import { join as pathJoin } from 'path'; import { join as pathJoin } from 'path';
// eslint-disable-next-line import/no-cycle
import { GenericHelpers } from '../..'; import { GenericHelpers } from '../..';
import * as config from '../../../config'; import * as config from '../../../config';
import { import {
@ -12,35 +13,33 @@ import {
} from './Interfaces'; } from './Interfaces';
import { NodeMailer } from './NodeMailer'; import { NodeMailer } from './NodeMailer';
// TODO: make function fully async (remove sync functions) type Template = HandlebarsTemplateDelegate<unknown>;
async function getTemplate(configKeyName: string, defaultFilename: string) { type TemplateName = 'invite' | 'passwordReset';
const templateOverride = (await GenericHelpers.getConfigValue(
`userManagement.emails.templates.${configKeyName}`,
)) as string;
let template; const templates: Partial<Record<TemplateName, Template>> = {};
if (templateOverride && existsSync(templateOverride)) {
template = readFileSync(templateOverride, { async function getTemplate(
encoding: 'utf-8', templateName: TemplateName,
}); defaultFilename = `${templateName}.html`,
} else { ): Promise<Template> {
template = readFileSync(pathJoin(__dirname, `templates/${defaultFilename}`), { let template = templates[templateName];
encoding: 'utf-8', if (!template) {
}); const templateOverride = (await GenericHelpers.getConfigValue(
`userManagement.emails.templates.${templateName}`,
)) as string;
let markup;
if (templateOverride && existsSync(templateOverride)) {
markup = await readFile(templateOverride, 'utf-8');
} else {
markup = await readFile(pathJoin(__dirname, `templates/${defaultFilename}`), 'utf-8');
}
template = Handlebars.compile(markup);
templates[templateName] = template;
} }
return template; return template;
} }
function replaceStrings(template: string, data: IDataObject) {
let output = template;
const keys = Object.keys(data);
keys.forEach((key) => {
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
output = output.replace(regex, data[key] as string);
});
return output;
}
export class UserManagementMailer { export class UserManagementMailer {
private mailer: UserManagementMailerImplementation | undefined; private mailer: UserManagementMailerImplementation | undefined;
@ -58,13 +57,13 @@ export class UserManagementMailer {
} }
async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> { async invite(inviteEmailData: InviteEmailData): Promise<SendEmailResult> {
let template = await getTemplate('invite', 'invite.html'); if (!this.mailer) return Promise.reject();
template = replaceStrings(template, inviteEmailData);
const result = await this.mailer?.sendMail({ const template = await getTemplate('invite');
const result = await this.mailer.sendMail({
emailRecipients: inviteEmailData.email, emailRecipients: inviteEmailData.email,
subject: 'You have been invited to n8n', subject: 'You have been invited to n8n',
body: template, body: template(inviteEmailData),
}); });
// If mailer does not exist it means mail has been disabled. // If mailer does not exist it means mail has been disabled.
@ -72,14 +71,13 @@ export class UserManagementMailer {
} }
async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> { async passwordReset(passwordResetData: PasswordResetData): Promise<SendEmailResult> {
let template = await getTemplate('passwordReset', 'passwordReset.html'); if (!this.mailer) return Promise.reject();
template = replaceStrings(template, passwordResetData);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const template = await getTemplate('passwordReset');
const result = await this.mailer?.sendMail({ const result = await this.mailer.sendMail({
emailRecipients: passwordResetData.email, emailRecipients: passwordResetData.email,
subject: 'n8n password reset', subject: 'n8n password reset',
body: template, body: template(passwordResetData),
}); });
// If mailer does not exist it means mail has been disabled. // If mailer does not exist it means mail has been disabled.