Skip to content

Commit

Permalink
feat(core): Switch to MJML for email templates
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy committed Aug 22, 2024
1 parent 84e54be commit ca6104a
Show file tree
Hide file tree
Showing 19 changed files with 708 additions and 74 deletions.
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"mjmlio.vscode-mjml",
"Vue.volar"
]
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"chokidar": "^3.5.2",
"concurrently": "^8.2.0",
"ioredis-mock": "^8.8.1",
"mjml": "^4.15.3",
"ts-essentials": "^7.0.3"
},
"dependencies": {
Expand Down
22 changes: 15 additions & 7 deletions packages/cli/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import shell from 'shelljs';
import { rawTimeZones } from '@vvo/tzdb';
import glob from 'fast-glob';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -13,21 +14,28 @@ const SPEC_THEME_FILENAME = 'swaggerTheme.css';

const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true';

copyUserManagementEmailTemplates();
generateUserManagementEmailTemplates();
generateTimezoneData();

if (publicApiEnabled) {
copySwaggerTheme();
bundleOpenApiSpecs();
}

function copyUserManagementEmailTemplates() {
const templates = {
source: path.resolve(ROOT_DIR, 'src', 'user-management', 'email', 'templates'),
destination: path.resolve(ROOT_DIR, 'dist', 'user-management', 'email'),
};
function generateUserManagementEmailTemplates() {
const sourceDir = path.resolve(ROOT_DIR, 'src', 'user-management', 'email', 'templates');
const destinationDir = path.resolve(ROOT_DIR, 'dist', 'user-management', 'email', 'templates');

shell.mkdir('-p', destinationDir);

shell.cp('-r', templates.source, templates.destination);
const templates = glob.sync('*.mjml', { cwd: sourceDir });
templates.forEach((template) => {
if (template.startsWith('_')) return;
const source = path.resolve(sourceDir, template);
const destination = path.resolve(destinationDir, template.replace(/\.mjml$/, '.handlebars'));
const command = `pnpm mjml --output ${destination} ${source}`;
shell.exec(command, { silent: false });
});
}

function copySwaggerTheme() {
Expand Down
5 changes: 1 addition & 4 deletions packages/cli/src/controllers/passwordReset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export class PasswordResetController {
private readonly authService: AuthService,
private readonly userService: UserService,
private readonly mfaService: MfaService,
private readonly urlService: UrlService,
private readonly license: License,
private readonly passwordUtility: PasswordUtility,
private readonly userRepository: UserRepository,
Expand Down Expand Up @@ -108,14 +107,12 @@ export class PasswordResetController {

const url = this.authService.generatePasswordResetUrl(user);

const { id, firstName, lastName } = user;
const { id, firstName } = user;
try {
await this.mailer.passwordReset({
email,
firstName,
lastName,
passwordResetUrl: url,
domain: this.urlService.getInstanceBaseUrl(),
});
} catch (error) {
this.eventService.emit('email-failed', {
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export class UserService {
const result = await this.mailer.invite({
email,
inviteAcceptUrl,
domain,
});
if (result.emailSent) {
invitedUser.user.emailSent = true;
Expand Down Expand Up @@ -168,7 +167,6 @@ export class UserService {
this.logger.error('Failed to send email', {
userId: owner.id,
inviteAcceptUrl,
domain,
email,
});
invitedUser.error = e.message;
Expand Down
7 changes: 1 addition & 6 deletions packages/cli/src/user-management/email/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
export type InviteEmailData = {
email: string;
firstName?: string;
lastName?: string;
inviteAcceptUrl: string;
domain: string;
};

export type PasswordResetData = {
email: string;
firstName?: string;
lastName?: string;
firstName: string;
passwordResetUrl: string;
domain: string;
};

export type SendEmailResult = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended';

import type { UrlService } from '@/services/url.service';
import type { InviteEmailData, PasswordResetData } from '@/user-management/email/Interfaces';
import { NodeMailer } from '@/user-management/email/node-mailer';
import { UserManagementMailer } from '@/user-management/email/user-management-mailer';
Expand Down Expand Up @@ -31,7 +32,7 @@ describe('UserManagementMailer', () => {
},
},
});
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock());
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock(), mock());

it('should not setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(false);
Expand All @@ -56,7 +57,18 @@ describe('UserManagementMailer', () => {
},
},
});
const userManagementMailer = new UserManagementMailer(config, mock(), mock(), mock());
const urlService = mock<UrlService>();
const userManagementMailer = new UserManagementMailer(
config,
mock(),
mock(),
urlService,
mock(),
);

beforeEach(() => {
urlService.getInstanceBaseUrl.mockReturnValue('https://n8n.url');
});

it('should setup email transport', async () => {
expect(userManagementMailer.isEmailSetUp).toBe(true);
Expand All @@ -67,9 +79,7 @@ describe('UserManagementMailer', () => {
const result = await userManagementMailer.invite(inviteEmailData);
expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining(
`<a href="${inviteEmailData.inviteAcceptUrl}" target="_blank">`,
),
body: expect.stringContaining(`href="${inviteEmailData.inviteAcceptUrl}"`),
emailRecipients: email,
subject: 'You have been invited to n8n',
});
Expand All @@ -79,7 +89,7 @@ describe('UserManagementMailer', () => {
const result = await userManagementMailer.passwordReset(passwordResetData);
expect(result.emailSent).toBe(true);
expect(nodeMailer.sendMail).toHaveBeenCalledWith({
body: expect.stringContaining(`<a href="${passwordResetData.passwordResetUrl}">`),
body: expect.stringContaining(`href="${passwordResetData.passwordResetUrl}"`),
emailRecipients: email,
subject: 'n8n password reset',
});
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/user-management/email/templates/_common.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<mj-head>
<mj-attributes>
<mj-all font-family="Helvetica, Open Sans, system-ui, sans-serif"></mj-all>
<mj-text
font-weight="400"
font-size="16px"
color="#444444"
line-height="24px"
padding="10px 0 0 0"
align="center"
></mj-text>
<mj-button
background-color="#ff6d5a"
color="#ffffff"
font-size="18px"
align="center"
padding-top="20px"
line-height="150%"
border-radius="4px"
></mj-button>
<mj-section padding="20px 0px"></mj-section>
</mj-attributes>
</mj-head>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6d5a"> A credential has been shared with you </mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text><b>"{{ credentialsName }}"</b> credential has been shared with you.</mj-text>
<mj-text>To access it, please click the button below. </mj-text>
<mj-button href="{{credentialsListUrl}}"> Open Credential </mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>

This file was deleted.

4 changes: 0 additions & 4 deletions packages/cli/src/user-management/email/templates/invite.html

This file was deleted.

17 changes: 17 additions & 0 deletions packages/cli/src/user-management/email/templates/invite.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6d5a"> Welcome to n8n! 🎉 </mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text>You have been invited to join n8n ({{domain}}).</mj-text>
<mj-text>To accept, please click the button below. </mj-text>
<mj-button href="{{inviteAcceptUrl}}"> Set Up Your Account </mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6d5a">Reset your n8n password </mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text font-size="20px">Hi {{firstName}},</mj-text>
<mj-text>Somebody asked to reset your password on n8n ({{ domain }}). </mj-text>
<mj-text
>Click the following link to choose a new password. <br />The link is valid for 20
minutes.
</mj-text>
<mj-button href="{{passwordResetUrl}}"> Set a new password </mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<mjml>
<mj-include path="./_common.mjml" />
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="24px" color="#ff6d5a"> A workflow has been shared with you </mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
<mj-column>
<mj-text><b>"{{ workflowName }}"</b> workflow has been shared with you.</mj-text>
<mj-text>To access it, please click the button below. </mj-text>
<mj-button href="{{workflowUrl}}"> Open Workflow </mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
Loading

0 comments on commit ca6104a

Please sign in to comment.