From 68746e22a02e0d031cdad6e4efa3968a1a14eaf1 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:26:38 +0530 Subject: [PATCH] Send Email Tool: Don't persist message on SMTP only connections (#19756) Previously this blocked users who only had SMTP configured to send outbound emails, this fixes it by making messageChannel and persist layer conditional --- .../email-tool/email-composer.service.ts | 69 ++++++++++++++----- .../tool/tools/email-tool/send-email-tool.ts | 12 ++-- .../email-tool/types/composed-email.type.ts | 3 +- .../resolvers/send-email.resolver.ts | 12 ++-- .../services/send-email.service.ts | 2 +- 5 files changed, 67 insertions(+), 31 deletions(-) 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 7646b2df8d..bd84d8fcb4 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 @@ -5,7 +5,11 @@ import { render, toPlainText } from '@react-email/render'; import DOMPurify from 'dompurify'; import { reactMarkupFromJSON } from 'twenty-emails'; import { MAX_EMAIL_RECIPIENTS } from 'twenty-shared/constants'; -import { type EmailAttachment, FileFolder } from 'twenty-shared/types'; +import { + ConnectedAccountProvider, + type EmailAttachment, + FileFolder, +} from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { In, type Repository } from 'typeorm'; import { z } from 'zod'; @@ -274,6 +278,27 @@ export class EmailComposerService { ); } + private async refreshConnectedAccountTokens( + connectedAccount: ConnectedAccountEntity, + workspaceId: string, + messageChannelId: string, + ): Promise { + const { accessToken, refreshToken } = + await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( + { + connectedAccount, + workspaceId, + messageChannelId, + }, + ); + + return { + ...connectedAccount, + accessToken, + refreshToken, + }; + } + async composeEmail( parameters: ComposeEmailParams, context: ToolExecutionContext, @@ -331,29 +356,34 @@ export class EmailComposerService { (channel) => channel.handle === connectedAccount.handle, ); - if (!isDefined(messageChannel)) { + const isSmtpOnlyAccount = + connectedAccount.provider === ConnectedAccountProvider.IMAP_SMTP_CALDAV && + !isDefined(connectedAccount.connectionParameters?.IMAP); + + if ( + isSmtpOnlyAccount && + !isDefined(connectedAccount.connectionParameters?.SMTP) + ) { + throw new EmailToolException( + `SMTP is not configured for connected account '${connectedAccountId}'`, + EmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + if (!isSmtpOnlyAccount && !isDefined(messageChannel)) { throw new EmailToolException( `No message channel found for connected account '${connectedAccountId}'`, EmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, ); } - const connectedAccountAsWorkspaceEntity = connectedAccount; - - const { accessToken, refreshToken } = - await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( - { - connectedAccount: connectedAccountAsWorkspaceEntity, + const connectedAccountWithFreshTokens = isDefined(messageChannel) + ? await this.refreshConnectedAccountTokens( + connectedAccount, workspaceId, - messageChannelId: messageChannel.id, - }, - ); - - const connectedAccountWithFreshTokens = { - ...connectedAccountAsWorkspaceEntity, - accessToken, - refreshToken, - }; + messageChannel.id, + ) + : connectedAccount; const attachments = await this.getAttachments( files || [], @@ -374,7 +404,7 @@ export class EmailComposerService { let threadExternalId: string | undefined; - if (inReplyTo) { + if (inReplyTo && isDefined(messageChannel)) { threadExternalId = await this.getThreadExternalId( workspaceId, inReplyTo, @@ -392,7 +422,8 @@ export class EmailComposerService { sanitizedHtmlBody, attachments, connectedAccount: connectedAccountWithFreshTokens, - messageChannelId: messageChannel.id, + messageChannelId: messageChannel?.id, + shouldPersistMessage: isDefined(messageChannel), inReplyTo, threadExternalId, }, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/send-email-tool.ts index 36eb06c38e..b698e8a950 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/send-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/send-email-tool.ts @@ -44,11 +44,13 @@ export class SendEmailTool implements Tool { const sendResult = await this.sendEmailService.sendComposedEmail(data); - await this.sendEmailService.persistSentMessage( - sendResult, - data, - context.workspaceId, - ); + if (data.shouldPersistMessage) { + await this.sendEmailService.persistSentMessage( + sendResult, + data, + context.workspaceId, + ); + } this.logger.log( `Email sent successfully to ${data.toRecipientsDisplay}${data.attachments.length > 0 ? ` with ${data.attachments.length} attachments` : ''}`, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/types/composed-email.type.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/types/composed-email.type.ts index 29a366ceab..aaa3d8f168 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/types/composed-email.type.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/types/composed-email.type.ts @@ -9,7 +9,8 @@ export type ComposedEmail = { sanitizedHtmlBody: string; attachments: MessageAttachment[]; connectedAccount: ConnectedAccountEntity; - messageChannelId: string; + messageChannelId?: string; + shouldPersistMessage: boolean; inReplyTo?: string; threadExternalId?: string; }; diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts index 9d0474bd6c..9d839cc77c 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/resolvers/send-email.resolver.ts @@ -79,11 +79,13 @@ export class SendEmailResolver { const sendResult = await this.sendEmailService.sendComposedEmail(data); - await this.sendEmailService.persistSentMessage( - sendResult, - data, - workspace.id, - ); + if (data.shouldPersistMessage) { + await this.sendEmailService.persistSentMessage( + sendResult, + data, + workspace.id, + ); + } const attachmentFileIds = (input.files ?? []).map((file) => file.id); diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/services/send-email.service.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/services/send-email.service.ts index 86e5e4d8af..f8b26f71cf 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/services/send-email.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/services/send-email.service.ts @@ -43,7 +43,7 @@ export class SendEmailService { body: data.plainTextBody, recipients: data.recipients, connectedAccount: data.connectedAccount, - messageChannelId: data.messageChannelId, + messageChannelId: data.messageChannelId!, inReplyTo: data.inReplyTo, workspaceId, });