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
This commit is contained in:
neo773
2026-04-17 18:26:38 +05:30
committed by GitHub
parent 4ed6fcd19e
commit 68746e22a0
5 changed files with 67 additions and 31 deletions
@@ -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<ConnectedAccountEntity> {
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,
},
@@ -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` : ''}`,
@@ -9,7 +9,8 @@ export type ComposedEmail = {
sanitizedHtmlBody: string;
attachments: MessageAttachment[];
connectedAccount: ConnectedAccountEntity;
messageChannelId: string;
messageChannelId?: string;
shouldPersistMessage: boolean;
inReplyTo?: string;
threadExternalId?: string;
};
@@ -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);
@@ -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,
});