diff --git a/packages/twenty-docs/docs.json b/packages/twenty-docs/docs.json index 60ba3cd4c7..2b997516d9 100644 --- a/packages/twenty-docs/docs.json +++ b/packages/twenty-docs/docs.json @@ -175,7 +175,8 @@ "user-guide/workflows/how-tos/crm-automations/formula-fields", "user-guide/workflows/how-tos/crm-automations/display-related-record-data", "user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -621,7 +622,8 @@ "l/fr/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/fr/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/fr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -1067,7 +1069,8 @@ "l/ar/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/ar/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/ar/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -1513,7 +1516,8 @@ "l/cs/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/cs/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/cs/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/cs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -1959,7 +1963,8 @@ "l/de/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/de/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/de/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -2405,7 +2410,8 @@ "l/es/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/es/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/es/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -2851,7 +2857,8 @@ "l/it/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/it/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/it/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -3297,7 +3304,8 @@ "l/ja/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/ja/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/ja/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -3743,7 +3751,8 @@ "l/ko/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/ko/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/ko/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/ko/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -4189,7 +4198,8 @@ "l/pt/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/pt/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/pt/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -4635,7 +4645,8 @@ "l/ro/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/ro/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/ro/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -5081,7 +5092,8 @@ "l/ru/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/ru/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/ru/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -5527,7 +5539,8 @@ "l/tr/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/tr/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/tr/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/tr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { @@ -5973,7 +5986,8 @@ "l/zh/user-guide/workflows/how-tos/crm-automations/formula-fields", "l/zh/user-guide/workflows/how-tos/crm-automations/display-related-record-data", "l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "l/zh/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/ai-triage.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/ai-triage.png new file mode 100644 index 0000000000..ed082df541 Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/ai-triage.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/find-sender.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/find-sender.png new file mode 100644 index 0000000000..c7bcb41aa9 Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/find-sender.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/gmail-threaded-reply.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/gmail-threaded-reply.png new file mode 100644 index 0000000000..5b19434110 Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/gmail-threaded-reply.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/search-records.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/search-records.png new file mode 100644 index 0000000000..4f122261c5 Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/search-records.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/send-email.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/send-email.png new file mode 100644 index 0000000000..1899648990 Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/send-email.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/should-reply.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/should-reply.png new file mode 100644 index 0000000000..daec20939e Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/should-reply.png differ diff --git a/packages/twenty-docs/images/user-guide/workflows/auto-reply/workflow-overview.png b/packages/twenty-docs/images/user-guide/workflows/auto-reply/workflow-overview.png new file mode 100644 index 0000000000..1a0a04b5de Binary files /dev/null and b/packages/twenty-docs/images/user-guide/workflows/auto-reply/workflow-overview.png differ diff --git a/packages/twenty-docs/navigation/base-structure.json b/packages/twenty-docs/navigation/base-structure.json index 3707010e57..fe476658cb 100644 --- a/packages/twenty-docs/navigation/base-structure.json +++ b/packages/twenty-docs/navigation/base-structure.json @@ -157,7 +157,8 @@ "user-guide/workflows/how-tos/crm-automations/formula-fields", "user-guide/workflows/how-tos/crm-automations/display-related-record-data", "user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { diff --git a/packages/twenty-docs/navigation/navigation-schema.json b/packages/twenty-docs/navigation/navigation-schema.json index bb95be1ab1..2ad19a5db1 100644 --- a/packages/twenty-docs/navigation/navigation-schema.json +++ b/packages/twenty-docs/navigation/navigation-schema.json @@ -156,7 +156,8 @@ "user-guide/workflows/how-tos/crm-automations/formula-fields", "user-guide/workflows/how-tos/crm-automations/display-related-record-data", "user-guide/workflows/how-tos/crm-automations/closed-won-automations", - "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities" + "user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities", + "user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails" ] }, { diff --git a/packages/twenty-docs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails.mdx b/packages/twenty-docs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails.mdx new file mode 100644 index 0000000000..8aa7105302 --- /dev/null +++ b/packages/twenty-docs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails.mdx @@ -0,0 +1,121 @@ +--- +title: Auto-Reply to Inbound Emails +description: Build a workflow that uses AI to triage incoming emails and send threaded replies automatically. +--- + +Respond to inbound emails in seconds — not hours. This workflow uses an AI Agent to filter out noise (newsletters, spam, auto-replies) and draft a personalized response to real messages, then sends it as a threaded reply inside the original conversation. + +## How email threading works + +Every email carries a hidden `Message-ID` header — a unique fingerprint assigned by the sender's mail server. When you reply to an email, your mail client sets an `In-Reply-To` header referencing that fingerprint. That's how Gmail, Outlook, and every other client groups messages into threads. + +In Twenty, that fingerprint is stored as `headerMessageId` on the Message object. Your workflow grabs it and passes it into the Send Email action's In-Reply-To field. + +## Building the workflow + +### Step 1: Create a new workflow + +Head to **Settings -> Workflows** and click **+ New Workflow**. + +### Step 2: Trigger on incoming messages + +Pick **When a Record is Created** and choose **Messages**. + +Every time an email lands in Twenty, this fires. + + + +### Step 3: Look up who sent it + +Add a **Search Records** action. + +The sender's address isn't on the message itself — it's on the related Message Participant record. + +| Field | Value | +|-------|-------| +| **Object** | Message Participants | +| **Filter** | Message **is** `{{trigger.id}}` | +| **Filter** | Role **is** From | +| **Limit** | 1 | + +This gives you the sender's email in `handle` and their name in `displayName`. + + + +### Step 4: AI triage and draft reply + +Add an **AI Agent** action. This single step does two things: decides whether the email deserves a reply, and if so, writes one. + +Use a prompt like: + +``` +You are an email triage assistant for a sales team. Read the following +inbound email and decide if it deserves a reply. + +Subject: {{trigger.subject}} +Body: {{trigger.text}} +From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}}) + +If this email is spam, a newsletter, an automated notification, or +otherwise does not need a human reply, respond with exactly: SKIP + +Otherwise, write a short, professional reply (3-4 sentences max) that: +- Acknowledges their specific message +- Lets them know someone from the team will follow up shortly +- Is warm but not overly casual + +Respond with only the reply text, no subject line or greeting prefix. +``` + +The AI Agent outputs its response in a `response` field that the next steps can reference. + + + +### Step 5: Branch on the AI decision + +Add an **If/Else** action to check whether the AI decided to reply or skip. + +| Field | Value | +|-------|-------| +| **Condition** | AI Agent `response` **does not contain** `SKIP` | +| **If true** | Continue to Send Email | +| **Else** | Do nothing (workflow ends) | + +Spam, newsletters, and auto-generated messages get dropped. Everything else moves to the next step. + + + +### Step 6: Send a threaded reply + +Add a **Send Email** action on the "if true" branch. Click **Advanced options**, then **Add In-Reply-To**. + +| Field | Value | +|-------|-------| +| **To** | `{{Find Sender.first.handle}}` | +| **Subject** | `Re: {{trigger.subject}}` | +| **Body** | `{{AI Triage & Draft Reply.response}}` | +| **In-Reply-To** | `{{trigger.headerMessageId}}` | + +The In-Reply-To field is what makes this a reply instead of a new conversation. The recipient sees it threaded under the original email in Gmail, Outlook, or any other client. + + + + +**In-Reply-To** expects a `message.headerMessageId` from the trigger — it's the email's unique fingerprint, not a recipient address. If you leave it empty, the email still sends, just as a standalone message. + + + +Gmail uses the subject line to group messages into threads. The subject **must** start with `Re:` (including the colon and space) for Gmail to display the reply inside the original thread. Without it, the reply will appear as a separate conversation — even if the In-Reply-To header is set correctly. + + +### Step 7: Test and activate + +Hit **Test**, then check your email client. The reply should appear nested under the original message. + +Activate when you're happy with it. + +## Ideas to build on + +- **Only reply to VIPs** — add a branch that checks the sender's domain or whether they exist as a Contact in Twenty +- **Route by intent** — use separate AI Agent prompts to handle sales inquiries differently from support requests +- **Enrich before replying** — add a Search Records step to pull the sender's company or deal history into the AI prompt for more personalized replies diff --git a/packages/twenty-front/src/modules/workflow/types/EmailFormData.ts b/packages/twenty-front/src/modules/workflow/types/EmailFormData.ts index bcdaecd58f..7d151c1bca 100644 --- a/packages/twenty-front/src/modules/workflow/types/EmailFormData.ts +++ b/packages/twenty-front/src/modules/workflow/types/EmailFormData.ts @@ -9,4 +9,5 @@ export type EmailFormData = { subject: string; body: string; files: WorkflowAttachment[]; + inReplyTo: string; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx index 56a4bde4cc..88bce32133 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx @@ -75,12 +75,14 @@ export const WorkflowEditActionEmailBase = ({ const [visibleAdvancedFields, setVisibleAdvancedFields] = useState<{ cc: boolean; bcc: boolean; + inReplyTo: boolean; }>(() => { const inputRecipients = action.settings.input.recipients; return { cc: Boolean(inputRecipients?.cc), bcc: Boolean(inputRecipients?.bcc), + inReplyTo: Boolean(action.settings.input.inReplyTo), }; }); @@ -89,7 +91,9 @@ export const WorkflowEditActionEmailBase = ({ const advancedOptionsDropdownId = `${action.id}-email-advanced-options`; const hasAvailableAdvancedOptions = - !visibleAdvancedFields.cc || !visibleAdvancedFields.bcc; + !visibleAdvancedFields.cc || + !visibleAdvancedFields.bcc || + !visibleAdvancedFields.inReplyTo; const handleReauthorize = async () => { if (!isDefined(missingScopes)) { @@ -276,6 +280,18 @@ export const WorkflowEditActionEmailBase = ({ VariablePicker={WorkflowVariablePicker} /> )} + {visibleAdvancedFields.inReplyTo && ( + { + handleFieldChange('inReplyTo', value); + }} + VariablePicker={WorkflowVariablePicker} + /> + )} {!actionOptions.readonly && hasAvailableAdvancedOptions && ( )} + {!visibleAdvancedFields.inReplyTo && ( + { + setVisibleAdvancedFields((prev) => ({ + ...prev, + inReplyTo: true, + })); + closeDropdown(advancedOptionsDropdownId); + }} + /> + )} } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx index c40970e0b2..8807606822 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx @@ -32,6 +32,7 @@ const DEFAULT_SEND_EMAIL_ACTION: WorkflowSendEmailAction = { subject: '', body: '', files: [], + inReplyTo: '', }, outputSchema: {}, errorHandlingOptions: { @@ -62,6 +63,7 @@ const CONFIGURED_SEND_EMAIL_ACTION: WorkflowSendEmailAction = { subject: 'Welcome to Twenty!', body: 'Dear Tim,\n\nWelcome to Twenty! We are excited to have you on board.\n\nBest regards,\nThe Team', files: [], + inReplyTo: '', }, outputSchema: {}, errorHandlingOptions: { @@ -91,6 +93,7 @@ const DEFAULT_DRAFT_EMAIL_ACTION: WorkflowDraftEmailAction = { subject: '', body: '', files: [], + inReplyTo: '', }, outputSchema: {}, errorHandlingOptions: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useEmailForm.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useEmailForm.ts index 9e83d19eeb..3cded844a4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useEmailForm.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useEmailForm.ts @@ -28,6 +28,7 @@ export const useEmailForm = ({ subject: action.settings.input.subject ?? '', body: action.settings.input.body ?? '', files: action.settings.input.files ?? [], + inReplyTo: action.settings.input.inReplyTo ?? '', }; }); @@ -46,6 +47,7 @@ export const useEmailForm = ({ subject: formData.subject, body: formData.body, files: formData.files, + inReplyTo: formData.inReplyTo, }, }, }); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/draft-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/draft-email-tool.ts index 9945c0dacb..2a42339495 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/draft-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/draft-email-tool.ts @@ -89,6 +89,7 @@ export class DraftEmailTool implements Tool { body: data.plainTextBody, html: data.sanitizedHtmlBody, attachments: data.attachments, + inReplyTo: data.inReplyTo, }, data.connectedAccount, ); 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 0ad458df32..12f059ed23 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 @@ -214,7 +214,7 @@ export class EmailComposerService { context: ToolExecutionContext, ): Promise { 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, }, }; } 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 374b36c061..4dce434f6a 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 @@ -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(), }); 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 dda6be2939..c5e0b37d27 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 @@ -88,6 +88,7 @@ export class SendEmailTool implements Tool { body: data.plainTextBody, html: data.sanitizedHtmlBody, attachments: data.attachments, + inReplyTo: data.inReplyTo, }, data.connectedAccount, ); 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 1b12f6ca85..679fe21cc6 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,4 +9,5 @@ export type ComposedEmail = { sanitizedHtmlBody: string; attachments: MessageAttachment[]; connectedAccount: ConnectedAccountWorkspaceEntity; + inReplyTo?: string; }; diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/microsoft/services/microsoft-message-outbound.service.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/microsoft/services/microsoft-message-outbound.service.ts index d3c2161b8f..6cdbc16f8d 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/microsoft/services/microsoft-message-outbound.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/microsoft/services/microsoft-message-outbound.service.ts @@ -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 { + 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 { + 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( diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/types/send-message-input.type.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/types/send-message-input.type.ts index a4a3575afd..f997f4feb0 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/types/send-message-input.type.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/types/send-message-input.type.ts @@ -12,4 +12,5 @@ export type SendMessageInput = { content: Buffer; contentType: string; }[]; + inReplyTo?: string; }; diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util.ts index c8557c11f3..c48e080705 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util.ts @@ -21,5 +21,11 @@ export const toMailComposerOptions = ( })), } : {}), + ...(sendMessageInput.inReplyTo + ? { + inReplyTo: sendMessageInput.inReplyTo, + references: sendMessageInput.inReplyTo, + } + : {}), }; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type.ts index 4e732f743f..1bc0e3aa4c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type.ts @@ -5,4 +5,5 @@ export type WorkflowSendEmailActionInput = { recipients: EmailRecipients; subject?: string; body?: string; + inReplyTo?: string; }; diff --git a/packages/twenty-shared/src/constants/DocumentationPaths.ts b/packages/twenty-shared/src/constants/DocumentationPaths.ts index 4e6d33b4b4..272e250a09 100644 --- a/packages/twenty-shared/src/constants/DocumentationPaths.ts +++ b/packages/twenty-shared/src/constants/DocumentationPaths.ts @@ -272,6 +272,8 @@ export const DOCUMENTATION_PATHS = { '/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty', USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_SET_UP_A_WEBHOOK_TRIGGER: '/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_AUTO_REPLY_TO_INBOUND_EMAILS: + '/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails', USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_CLOSED_WON_AUTOMATIONS: '/user-guide/workflows/how-tos/crm-automations/closed-won-automations', USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_DETECT_STALE_OPPORTUNITIES: diff --git a/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts index 2970183154..34643797b2 100644 --- a/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/send-email-action-settings-schema.ts @@ -14,5 +14,6 @@ export const workflowSendEmailActionSettingsSchema = subject: z.string().optional(), body: z.string().optional(), files: z.array(workflowFileSchema).optional().default([]), + inReplyTo: z.string().trim().optional(), }), });