Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Switch to MJML for email templates #10518

Merged
merged 22 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
60104fb
feat(core): Switch to MJML for email templates
netroy Aug 22, 2024
78f1f2f
fix button line-height
netroy Aug 23, 2024
c4e021d
change button text to sentence case
netroy Aug 23, 2024
5499dcc
rename template files to kebab-case
netroy Aug 23, 2024
b3c9362
Update user-invited.mjml
netroy Aug 23, 2024
bf2333e
Update password-reset-requested.mjml
netroy Aug 23, 2024
5d24a01
use the destructured base payload first
netroy Aug 23, 2024
062c67a
Merge remote-tracking branch 'origin/master' into PAY-1807-mjml-templ…
netroy Aug 23, 2024
7c81fb2
add n8n logo at the bottom of every email
netroy Aug 23, 2024
c4a9376
use correct n8n colors
netroy Aug 23, 2024
928db25
use the correct font-stack
netroy Aug 23, 2024
afee928
inline the logo
netroy Aug 23, 2024
6e2089a
add a background color to "fix" dark mode contrast issues
netroy Aug 23, 2024
f58a2ad
add a notice to the password-reset email
netroy Aug 23, 2024
9da8536
fix config test
netroy Aug 23, 2024
b3114bf
Merge remote-tracking branch 'origin/master' into PAY-1807-mjml-templ…
netroy Aug 27, 2024
e4f415b
add a space before punctuation to avoid it messing up the domain rend…
netroy Aug 27, 2024
262eb47
use a png image instead for the logo
netroy Aug 27, 2024
5b30cc8
highlight domain and notes better in email templates
netroy Aug 27, 2024
4425309
use an inline image instead of a data-uri for logo
netroy Aug 27, 2024
bbb2bc2
Merge remote-tracking branch 'origin/master' into PAY-1807-mjml-templ…
netroy Aug 27, 2024
a8c6d98
Merge remote-tracking branch 'origin/master' into PAY-1807-mjml-templ…
netroy Aug 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 1 addition & 5 deletions packages/cli/src/controllers/passwordReset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { MfaService } from '@/mfa/mfa.service';
import { Logger } from '@/logger';
import { ExternalHooks } from '@/external-hooks';
import { UrlService } from '@/services/url.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
Expand All @@ -31,7 +30,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 +106,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>
netroy marked this conversation as resolved.
Show resolved Hide resolved
<mj-section padding="20px 0px"></mj-section>
</mj-attributes>
</mj-head>

This file was deleted.

netroy marked this conversation as resolved.
Show resolved Hide resolved
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>
netroy marked this conversation as resolved.
Show resolved Hide resolved
</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>
netroy marked this conversation as resolved.
Show resolved Hide resolved
<mj-text>To accept, please click the button below. </mj-text>
<mj-button href="{{inviteAcceptUrl}}"> Set Up Your Account </mj-button>
</mj-column>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra sentence/link somewhere explaining to the invitee what even is n8n?

</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>
netroy marked this conversation as resolved.
Show resolved Hide resolved
<mj-text
>Click the following link to choose a new password. <br />The link is valid for 20
minutes.
netroy marked this conversation as resolved.
Show resolved Hide resolved
</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>
netroy marked this conversation as resolved.
Show resolved Hide resolved
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF" border="1px solid #ddd">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not familiar with MJML, maybe there's a way to extract common styles to _common and use CSS classes instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while it is possible, I've always struggled it do that in the past. Also, since these are emails, all those styles are likely to be inlined anyways.
I'd like to improve this whole setup, but perhaps in another PR.

<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
Loading