Add In-Reply-To to Email Workflow Node (#18641)

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2026-03-22 21:19:08 +05:30
committed by GitHub
parent 7bde8a4dfa
commit 8ef32c4781
26 changed files with 259 additions and 27 deletions
@@ -89,6 +89,7 @@ export class DraftEmailTool implements Tool {
body: data.plainTextBody,
html: data.sanitizedHtmlBody,
attachments: data.attachments,
inReplyTo: data.inReplyTo,
},
data.connectedAccount,
);
@@ -214,7 +214,7 @@ export class EmailComposerService {
context: ToolExecutionContext,
): Promise<EmailComposerResult> {
const { workspaceId } = context;
const { subject, body, files } = parameters;
const { subject, body, files, inReplyTo } = parameters;
let { connectedAccountId } = parameters;
let recipients: { to: string[]; cc: string[]; bcc: string[] };
@@ -309,6 +309,7 @@ export class EmailComposerService {
sanitizedHtmlBody,
attachments,
connectedAccount: connectedAccountWithFreshTokens,
inReplyTo,
},
};
}
@@ -37,4 +37,10 @@ export const EmailToolInputZodSchema = z.object({
.describe('Array of file objects to attach to the email')
.optional()
.default([]),
inReplyTo: z
.string()
.describe(
'The RFC 2822 Message-ID of an existing email to reply to. When provided, the email is sent as a reply in the same thread.',
)
.optional(),
});
@@ -88,6 +88,7 @@ export class SendEmailTool implements Tool {
body: data.plainTextBody,
html: data.sanitizedHtmlBody,
attachments: data.attachments,
inReplyTo: data.inReplyTo,
},
data.connectedAccount,
);
@@ -9,4 +9,5 @@ export type ComposedEmail = {
sanitizedHtmlBody: string;
attachments: MessageAttachment[];
connectedAccount: ConnectedAccountWorkspaceEntity;
inReplyTo?: string;
};
@@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import { z } from 'zod';
import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbound-manager/interfaces/message-outbound-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { toMicrosoftRecipients } from 'src/modules/messaging/message-import-manager/utils/to-microsoft-recipients.util';
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
import { type Client as MicrosoftGraphClient } from '@microsoft/microsoft-graph-client';
import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
@@ -24,13 +24,12 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
connectedAccount,
);
const message = this.composeMicrosoftMessage(sendMessageInput);
const messageId = await this.createDraftMessage(
microsoftClient,
sendMessageInput,
);
const response = await microsoftClient.api(`/me/messages`).post(message);
z.string().parse(response.id);
await microsoftClient.api(`/me/messages/${response.id}/send`).post({});
await microsoftClient.api(`/me/messages/${messageId}/send`).post({});
}
async createDraft(
@@ -42,9 +41,50 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
connectedAccount,
);
await this.createDraftMessage(microsoftClient, sendMessageInput);
}
private async createDraftMessage(
microsoftClient: MicrosoftGraphClient,
sendMessageInput: SendMessageInput,
): Promise<string> {
const parentMessageGraphId = sendMessageInput.inReplyTo
? await this.findMessageByInternetMessageId(
microsoftClient,
sendMessageInput.inReplyTo,
)
: undefined;
const message = this.composeMicrosoftMessage(sendMessageInput);
await microsoftClient.api(`/me/messages`).post(message);
if (isDefined(parentMessageGraphId)) {
const reply = await microsoftClient
.api(`/me/messages/${parentMessageGraphId}/createReply`)
.post({});
await microsoftClient.api(`/me/messages/${reply.id}`).patch(message);
return reply.id;
}
const response = await microsoftClient.api('/me/messages').post(message);
return response.id;
}
private async findMessageByInternetMessageId(
microsoftClient: MicrosoftGraphClient,
internetMessageId: string,
): Promise<string | undefined> {
const encodedId = encodeURIComponent(internetMessageId);
const response = await microsoftClient
.api(
`/me/messages?$filter=internetMessageId eq '${encodedId}'&$select=id&$top=1`,
)
.get();
return response?.value?.[0]?.id;
}
private composeMicrosoftMessage(
@@ -12,4 +12,5 @@ export type SendMessageInput = {
content: Buffer;
contentType: string;
}[];
inReplyTo?: string;
};
@@ -21,5 +21,11 @@ export const toMailComposerOptions = (
})),
}
: {}),
...(sendMessageInput.inReplyTo
? {
inReplyTo: sendMessageInput.inReplyTo,
references: sendMessageInput.inReplyTo,
}
: {}),
};
};
@@ -5,4 +5,5 @@ export type WorkflowSendEmailActionInput = {
recipients: EmailRecipients;
subject?: string;
body?: string;
inReplyTo?: string;
};