feat: rich text email body (#14482)
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+2
-2
@@ -80,8 +80,8 @@ export class ApprovedAccessDomainService {
|
||||
serverUrl: this.twentyConfigService.get('SERVER_URL'),
|
||||
locale: sender.locale,
|
||||
});
|
||||
const html = render(emailTemplate);
|
||||
const text = render(emailTemplate, {
|
||||
const html = await render(emailTemplate);
|
||||
const text = await render(emailTemplate, {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
|
||||
+11
@@ -18,6 +18,17 @@ import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-membe
|
||||
|
||||
import { ApprovedAccessDomainService } from './approved-access-domain.service';
|
||||
|
||||
// To avoid dynamic import issues in Jest
|
||||
jest.mock('@react-email/render', () => ({
|
||||
render: jest.fn().mockImplementation(async (template, options) => {
|
||||
if (options?.plainText) {
|
||||
return 'Plain Text Email';
|
||||
}
|
||||
|
||||
return '<html><body>HTML email content</body></html>';
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ApprovedAccessDomainService', () => {
|
||||
let service: ApprovedAccessDomainService;
|
||||
let approvedAccessDomainRepository: Repository<ApprovedAccessDomain>;
|
||||
|
||||
@@ -482,8 +482,8 @@ export class AuthService {
|
||||
locale: firstUserWorkspace.locale,
|
||||
});
|
||||
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
const html = await render(emailTemplate, { pretty: true });
|
||||
const text = await render(emailTemplate, { plainText: true });
|
||||
|
||||
const passwordChangedMsg = msg`Your Password Has Been Successfully Changed`;
|
||||
const i18n = this.i18nService.getI18nInstance(firstUserWorkspace.locale);
|
||||
|
||||
+11
@@ -18,6 +18,17 @@ import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
|
||||
// To avoid dynamic import issues in Jest
|
||||
jest.mock('@react-email/render', () => ({
|
||||
render: jest.fn().mockImplementation(async (template, options) => {
|
||||
if (options?.plainText) {
|
||||
return 'Plain Text Email';
|
||||
}
|
||||
|
||||
return '<html><body>HTML email content</body></html>';
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ResetPasswordService', () => {
|
||||
let service: ResetPasswordService;
|
||||
let userRepository: Repository<User>;
|
||||
|
||||
+2
-2
@@ -163,8 +163,8 @@ export class ResetPasswordService {
|
||||
|
||||
const emailTemplate = PasswordResetLinkEmail(emailData);
|
||||
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
const html = await render(emailTemplate, { pretty: true });
|
||||
const text = await render(emailTemplate, { plainText: true });
|
||||
|
||||
const resetPasswordMsg = msg`Action Needed to Reset Password`;
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
|
||||
+2
-2
@@ -80,8 +80,8 @@ export class EmailVerificationService {
|
||||
|
||||
const emailTemplate = SendEmailVerificationLinkEmail(emailData);
|
||||
|
||||
const html = render(emailTemplate);
|
||||
const text = render(emailTemplate, {
|
||||
const html = await render(emailTemplate);
|
||||
const text = await render(emailTemplate, {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
|
||||
+11
-2
@@ -1,6 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { render, toPlainText } from '@react-email/render';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { reactMarkupFromJSON } from 'twenty-emails';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -16,6 +18,7 @@ import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/s
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
|
||||
import { parseEmailBody } from 'src/utils/parse-email-body';
|
||||
|
||||
@Injectable()
|
||||
export class SendEmailTool implements Tool {
|
||||
@@ -115,17 +118,23 @@ export class SendEmailTool implements Tool {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const parsedBody = parseEmailBody(body);
|
||||
const reactMarkup = reactMarkupFromJSON(parsedBody);
|
||||
const htmlBody = await render(reactMarkup);
|
||||
const textBody = toPlainText(htmlBody);
|
||||
|
||||
const { JSDOM } = await import('jsdom');
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
const safeBody = purify.sanitize(body || '');
|
||||
const safeHtmlBody = purify.sanitize(htmlBody || '');
|
||||
const safeSubject = purify.sanitize(subject || '');
|
||||
|
||||
await this.sendMessageService.sendMessage(
|
||||
{
|
||||
to: email,
|
||||
subject: safeSubject,
|
||||
body: safeBody,
|
||||
body: textBody,
|
||||
html: safeHtmlBody,
|
||||
},
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
+11
@@ -23,6 +23,17 @@ import { WorkspaceInvitationService } from './workspace-invitation.service';
|
||||
// To fix a circular dependency issue
|
||||
jest.mock('src/engine/core-modules/workspace/services/workspace.service');
|
||||
|
||||
// To avoid dynamic import issues in Jest
|
||||
jest.mock('@react-email/render', () => ({
|
||||
render: jest.fn().mockImplementation(async (template, options) => {
|
||||
if (options?.plainText) {
|
||||
return 'Plain Text Email';
|
||||
}
|
||||
|
||||
return '<html><body>HTML email content</body></html>';
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('WorkspaceInvitationService', () => {
|
||||
let service: WorkspaceInvitationService;
|
||||
let appTokenRepository: Repository<AppToken>;
|
||||
|
||||
+2
-2
@@ -305,8 +305,8 @@ export class WorkspaceInvitationService {
|
||||
};
|
||||
|
||||
const emailTemplate = SendInviteLinkEmail(emailData);
|
||||
const html = render(emailTemplate);
|
||||
const text = render(emailTemplate, {
|
||||
const html = await render(emailTemplate);
|
||||
const text = await render(emailTemplate, {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
|
||||
+4
-4
@@ -123,8 +123,8 @@ export class CleanerWorkspaceService {
|
||||
locale: workspaceMember.locale,
|
||||
};
|
||||
const emailTemplate = WarnSuspendedWorkspaceEmail(emailData);
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
const html = await render(emailTemplate, { pretty: true });
|
||||
const text = await render(emailTemplate, { plainText: true });
|
||||
|
||||
const workspaceDeletionMsg = msg`Action needed to prevent workspace deletion`;
|
||||
const i18n = this.i18nService.getI18nInstance(workspaceMember.locale);
|
||||
@@ -201,8 +201,8 @@ export class CleanerWorkspaceService {
|
||||
locale: workspaceMember.locale,
|
||||
};
|
||||
const emailTemplate = CleanSuspendedWorkspaceEmail(emailData);
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
const html = await render(emailTemplate, { pretty: true });
|
||||
const text = await render(emailTemplate, { plainText: true });
|
||||
|
||||
this.emailService.send({
|
||||
to: workspaceMember.userEmail,
|
||||
|
||||
+2
@@ -41,6 +41,7 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = {
|
||||
company: '20202020-ceab-4a28-b546-73b06b4c08d5',
|
||||
opportunity: '20202020-7374-499d-bea3-9354890755b5',
|
||||
dashboard: '20202020-5324-43f3-9dbf-1a33e7de0ce6',
|
||||
workflow: '20202020-f1e8-4c9d-8a7b-3f5e1d2c9a8b',
|
||||
custom: '20202020-302d-43b3-9aea-aa4f89282a9f',
|
||||
} as const;
|
||||
|
||||
@@ -464,6 +465,7 @@ export const WORKFLOW_STANDARD_FIELD_IDS = {
|
||||
automatedTriggers: '20202020-3319-4234-a34c-117ecad2b8a9',
|
||||
favorites: '20202020-c554-4c41-be7a-cf9cd4b0d512',
|
||||
timelineActivities: '20202020-906e-486a-a798-131a5f081faf',
|
||||
attachments: '20202020-4a8c-4e2d-9b1c-7e5f3a2b4c6d',
|
||||
createdBy: '20202020-6007-401a-8aa5-e6f48581a6f3',
|
||||
searchVector: '20202020-535d-4ffa-b7f3-4fa0d5da1b7a',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user