7417 workflows i can send emails using the email account (#7431)

- update `send-email.workflow-action.ts` so it send email via the google
sdk
- remove useless `workflow-action.email.ts`
- add `send` authorization to google api scopes
- update the front workflow email step form to provide a
`connectedAccountId` from the available connected accounts
- update the permissions of connected accounts: ask users to reconnect
when selecting missing send permission


![image](https://github.com/user-attachments/assets/fe3c329d-fd67-4d0d-8450-099c35933645)
This commit is contained in:
martmull
2024-10-08 23:29:09 +02:00
committed by GitHub
parent 444cd3f03f
commit f138a1cf6e
30 changed files with 443 additions and 159 deletions

View File

@ -99,6 +99,16 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
})
handleAliases: string;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.scopes,
type: FieldMetadataType.ARRAY,
label: 'Scopes',
description: 'Scopes',
icon: 'IconSettings',
})
@WorkspaceIsNullable()
scopes: string[] | null;
@WorkspaceRelation({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.accountOwner,
type: RelationMetadataType.MANY_TO_ONE,

View File

@ -0,0 +1,13 @@
import { CustomException } from 'src/utils/custom-exception';
export class MailSenderException extends CustomException {
code: MailSenderExceptionCode;
constructor(message: string, code: MailSenderExceptionCode) {
super(message, code);
}
}
export enum MailSenderExceptionCode {
PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED',
CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND',
}

View File

@ -4,13 +4,24 @@ import { z } from 'zod';
import Handlebars from 'handlebars';
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
import { WorkflowActionEmail } from 'twenty-emails';
import { render } from '@react-email/components';
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type';
import { WorkflowSendEmailStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import {
MailSenderException,
MailSenderExceptionCode,
} from 'src/modules/mail-sender/exceptions/mail-sender.exception';
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { isDefined } from 'src/utils/is-defined';
@Injectable()
export class SendEmailWorkflowAction {
@ -18,8 +29,48 @@ export class SendEmailWorkflowAction {
constructor(
private readonly environmentService: EnvironmentService,
private readonly emailService: EmailService,
private readonly gmailClientProvider: GmailClientProvider,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
private async getEmailClient(step: WorkflowSendEmailStep) {
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) {
throw new WorkflowStepExecutorException(
'Scoped workspace not found',
WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND,
);
}
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOneBy({
id: step.settings.connectedAccountId,
});
if (!isDefined(connectedAccount)) {
throw new MailSenderException(
`Connected Account '${step.settings.connectedAccountId}' not found`,
MailSenderExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
switch (connectedAccount.provider) {
case 'google':
return await this.gmailClientProvider.getGmailClient(connectedAccount);
default:
throw new MailSenderException(
`Provider ${connectedAccount.provider} is not supported`,
MailSenderExceptionCode.PROVIDER_NOT_SUPPORTED,
);
}
}
async execute({
step,
payload,
@ -30,6 +81,8 @@ export class SendEmailWorkflowAction {
[key: string]: string;
};
}): Promise<WorkflowActionResult> {
const emailProvider = await this.getEmailClient(step);
try {
const emailSchema = z.string().trim().email('Invalid email');
@ -41,33 +94,33 @@ export class SendEmailWorkflowAction {
return { result: { success: false } };
}
const mainText = Handlebars.compile(step.settings.template)(payload);
const body = Handlebars.compile(step.settings.body)(payload);
const subject = Handlebars.compile(step.settings.subject)(payload);
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const safeHTML = purify.sanitize(mainText || '');
const safeBody = purify.sanitize(body || '');
const safeSubject = purify.sanitize(subject || '');
const email = WorkflowActionEmail({
dangerousHTML: safeHTML,
title: step.settings.title,
callToAction: step.settings.callToAction,
});
const html = render(email, {
pretty: true,
});
const text = render(email, {
plainText: true,
const message = [
`To: ${payload.email}`,
`Subject: ${safeSubject || ''}`,
'MIME-Version: 1.0',
'Content-Type: text/plain; charset="UTF-8"',
'',
safeBody,
].join('\n');
const encodedMessage = Buffer.from(message).toString('base64');
await emailProvider.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage,
},
});
await this.emailService.send({
from: `${this.environmentService.get(
'EMAIL_FROM_NAME',
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
to: payload.email,
subject: step.settings.subject || '',
text,
html,
});
this.logger.log(`Email sent successfully`);
return { result: { success: true } };
} catch (error) {

View File

@ -42,6 +42,10 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
GmailGetMessageListService,
GmailHandleErrorService,
],
exports: [GmailGetMessagesService, GmailGetMessageListService],
exports: [
GmailGetMessagesService,
GmailGetMessageListService,
GmailClientProvider,
],
})
export class MessagingGmailDriverModule {}

View File

@ -14,11 +14,7 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
};
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
connectedAccountId: string;
subject?: string;
template?: string;
title?: string;
callToAction?: {
value: string;
href: string;
};
body?: string;
};

View File

@ -7,9 +7,14 @@ import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code
import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
@Module({
imports: [WorkflowCommonModule, ServerlessFunctionModule],
imports: [
WorkflowCommonModule,
ServerlessFunctionModule,
MessagingGmailDriverModule,
],
providers: [
WorkflowExecutorWorkspaceService,
ScopedWorkspaceContextFactory,