diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailComposerFields.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailComposerFields.tsx index f99933d5ff..8ae0cbb18c 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailComposerFields.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailComposerFields.tsx @@ -111,7 +111,7 @@ export const EmailComposerFields = ({ placeholder={t`Type something or press "/" to see commands`} minHeight={120} maxWidth={600} - contentType="json" + contentType="html" /> { - if (contentType === 'markdown') { + if (contentType === 'markdown' || contentType === 'html') { onChange(editor.getHTML()); } else { const jsonContent = editor.getJSON(); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts index bd84d8fcb4..8e3e367075 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts @@ -1,9 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { render, toPlainText } from '@react-email/render'; +import { toPlainText } from '@react-email/render'; import DOMPurify from 'dompurify'; -import { reactMarkupFromJSON } from 'twenty-emails'; import { MAX_EMAIL_RECIPIENTS } from 'twenty-shared/constants'; import { ConnectedAccountProvider, @@ -31,7 +30,6 @@ import { MessagingAccountAuthenticationService } from 'src/modules/messaging/mes import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message'; -import { parseEmailBody } from 'src/utils/parse-email-body'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; @Injectable() export class EmailComposerService { @@ -391,15 +389,12 @@ export class EmailComposerService { options.attachmentsFileFolder, ); - const parsedBody = parseEmailBody(body); - const reactMarkup = reactMarkupFromJSON(parsedBody); - const htmlBody = await render(reactMarkup); - const plainTextBody = toPlainText(htmlBody); - const { JSDOM } = await import('jsdom'); const window = new JSDOM('').window; const purify = DOMPurify(window); - const sanitizedHtmlBody = purify.sanitize(htmlBody || ''); + + const sanitizedHtmlBody = purify.sanitize(body || ''); + const plainTextBody = toPlainText(sanitizedHtmlBody); const sanitizedSubject = purify.sanitize(subject || ''); let threadExternalId: string | undefined; diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-tool.schema.ts index 4dce434f6a..8fd92a4592 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-tool.schema.ts @@ -24,7 +24,7 @@ export const EmailToolInputZodSchema = z.object({ 'Recipients object with to, cc, and bcc fields (comma-separated)', ), subject: z.string().describe('The email subject line'), - body: z.string().describe('The email body content (HTML or plain text)'), + body: z.string().describe('The email body content in HTML format'), connectedAccountId: z .string() .refine((val) => isValidUuid(val)) diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util.ts new file mode 100644 index 0000000000..50cfb8f2ba --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util.ts @@ -0,0 +1,10 @@ +import { render } from '@react-email/render'; +import { type JSONContent, reactMarkupFromJSON } from 'twenty-emails'; + +export const renderRichTextToHtml = async ( + jsonContent: JSONContent, +): Promise => { + const reactMarkup = reactMarkupFromJSON(jsonContent); + + return render(reactMarkup); +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts index 401335c933..b096ddc5e4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts @@ -5,6 +5,7 @@ import { resolveInput, resolveRichTextVariables } from 'twenty-shared/utils'; import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool'; +import { renderRichTextToHtml } from 'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util'; import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool'; import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; @@ -67,9 +68,13 @@ export class ToolExecutorWorkflowAction implements WorkflowAction { const emailInput = toolInput as WorkflowSendEmailActionInput; if (emailInput.body) { + const resolvedBody = resolveRichTextVariables(emailInput.body, context); + const bodyJson = JSON.parse(resolvedBody!); + const htmlBody = await renderRichTextToHtml(bodyJson); + toolInput = { ...emailInput, - body: resolveRichTextVariables(emailInput.body, context), + body: htmlBody, }; } } diff --git a/packages/twenty-server/src/utils/__test__/parse-email-body.spec.ts b/packages/twenty-server/src/utils/__test__/parse-email-body.spec.ts deleted file mode 100644 index 7c6b5b5e58..0000000000 --- a/packages/twenty-server/src/utils/__test__/parse-email-body.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { parseEmailBody } from 'src/utils/parse-email-body'; - -describe('parseEmailBody', () => { - it('should parse valid JSON content', () => { - const jsonContent = { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: 'Hello World' }], - }, - ], - }; - - const result = parseEmailBody(JSON.stringify(jsonContent)); - - expect(result).toEqual(jsonContent); - }); - - it('should return plain string when JSON parsing fails', () => { - const plainText = 'This is plain text, not JSON'; - - const result = parseEmailBody(plainText); - - expect(result).toBe(plainText); - }); - - it('should parse JSON content with hardBreak nodes', () => { - const jsonWithHardBreaks = { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { type: 'text', text: 'Line 1' }, - { type: 'hardBreak' }, - { type: 'text', text: 'Line 2' }, - ], - }, - ], - }; - - const result = parseEmailBody(JSON.stringify(jsonWithHardBreaks)); - - expect(result).toEqual(jsonWithHardBreaks); - expect( - (result as typeof jsonWithHardBreaks).content[0].content, - ).toContainEqual({ - type: 'hardBreak', - }); - }); - - it('should handle empty string', () => { - const result = parseEmailBody(''); - - expect(result).toBe(''); - }); - - it('should handle JSON array format', () => { - const arrayContent = [ - { - type: 'paragraph', - content: [{ type: 'text', text: 'Content' }], - }, - ]; - - const result = parseEmailBody(JSON.stringify(arrayContent)); - - expect(result).toEqual(arrayContent); - }); -}); diff --git a/packages/twenty-server/src/utils/parse-email-body.ts b/packages/twenty-server/src/utils/parse-email-body.ts deleted file mode 100644 index 111d561941..0000000000 --- a/packages/twenty-server/src/utils/parse-email-body.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type JSONContent } from 'twenty-emails'; - -export const parseEmailBody = (body: string): JSONContent | string => { - try { - const json = JSON.parse(body); - - return json; - } catch { - return body; - } -};