Skip to content

Commit b0fa3e8

Browse files
authored
improvement(reply-gmail): added reply to gmail (#1809)
* added reply to thread/message * cleanup, extract header helper for threaded replies * more helpers
1 parent f65d62e commit b0fa3e8

File tree

9 files changed

+241
-54
lines changed

9 files changed

+241
-54
lines changed

apps/sim/app/api/tools/gmail/draft/route.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { createLogger } from '@/lib/logs/console/logger'
55
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
66
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
77
import { generateRequestId } from '@/lib/utils'
8-
import { base64UrlEncode, buildMimeMessage } from '@/tools/gmail/utils'
8+
import {
9+
base64UrlEncode,
10+
buildMimeMessage,
11+
buildSimpleEmailMessage,
12+
fetchThreadingHeaders,
13+
} from '@/tools/gmail/utils'
914

1015
export const dynamic = 'force-dynamic'
1116

@@ -16,8 +21,10 @@ const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
1621
const GmailDraftSchema = z.object({
1722
accessToken: z.string().min(1, 'Access token is required'),
1823
to: z.string().min(1, 'Recipient email is required'),
19-
subject: z.string().min(1, 'Subject is required'),
24+
subject: z.string().optional().nullable(),
2025
body: z.string().min(1, 'Email body is required'),
26+
threadId: z.string().optional().nullable(),
27+
replyToMessageId: z.string().optional().nullable(),
2128
cc: z.string().optional().nullable(),
2229
bcc: z.string().optional().nullable(),
2330
attachments: z.array(z.any()).optional().nullable(),
@@ -49,11 +56,19 @@ export async function POST(request: NextRequest) {
4956

5057
logger.info(`[${requestId}] Creating Gmail draft`, {
5158
to: validatedData.to,
52-
subject: validatedData.subject,
59+
subject: validatedData.subject || '',
5360
hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0),
5461
attachmentCount: validatedData.attachments?.length || 0,
5562
})
5663

64+
const threadingHeaders = validatedData.replyToMessageId
65+
? await fetchThreadingHeaders(validatedData.replyToMessageId, validatedData.accessToken)
66+
: {}
67+
68+
const originalMessageId = threadingHeaders.messageId
69+
const originalReferences = threadingHeaders.references
70+
const originalSubject = threadingHeaders.subject
71+
5772
let rawMessage: string | undefined
5873

5974
if (validatedData.attachments && validatedData.attachments.length > 0) {
@@ -106,8 +121,10 @@ export async function POST(request: NextRequest) {
106121
to: validatedData.to,
107122
cc: validatedData.cc ?? undefined,
108123
bcc: validatedData.bcc ?? undefined,
109-
subject: validatedData.subject,
124+
subject: validatedData.subject || originalSubject || '',
110125
body: validatedData.body,
126+
inReplyTo: originalMessageId,
127+
references: originalReferences,
111128
attachments: attachmentBuffers,
112129
})
113130

@@ -117,22 +134,21 @@ export async function POST(request: NextRequest) {
117134
}
118135

119136
if (!rawMessage) {
120-
const emailHeaders = [
121-
'Content-Type: text/plain; charset="UTF-8"',
122-
'MIME-Version: 1.0',
123-
`To: ${validatedData.to}`,
124-
]
125-
126-
if (validatedData.cc) {
127-
emailHeaders.push(`Cc: ${validatedData.cc}`)
128-
}
129-
if (validatedData.bcc) {
130-
emailHeaders.push(`Bcc: ${validatedData.bcc}`)
131-
}
137+
rawMessage = buildSimpleEmailMessage({
138+
to: validatedData.to,
139+
cc: validatedData.cc,
140+
bcc: validatedData.bcc,
141+
subject: validatedData.subject || originalSubject,
142+
body: validatedData.body,
143+
inReplyTo: originalMessageId,
144+
references: originalReferences,
145+
})
146+
}
147+
148+
const draftMessage: { raw: string; threadId?: string } = { raw: rawMessage }
132149

133-
emailHeaders.push(`Subject: ${validatedData.subject}`, '', validatedData.body)
134-
const email = emailHeaders.join('\n')
135-
rawMessage = Buffer.from(email).toString('base64url')
150+
if (validatedData.threadId) {
151+
draftMessage.threadId = validatedData.threadId
136152
}
137153

138154
const gmailResponse = await fetch(`${GMAIL_API_BASE}/drafts`, {
@@ -142,7 +158,7 @@ export async function POST(request: NextRequest) {
142158
'Content-Type': 'application/json',
143159
},
144160
body: JSON.stringify({
145-
message: { raw: rawMessage },
161+
message: draftMessage,
146162
}),
147163
})
148164

apps/sim/app/api/tools/gmail/send/route.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { createLogger } from '@/lib/logs/console/logger'
55
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
66
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
77
import { generateRequestId } from '@/lib/utils'
8-
import { base64UrlEncode, buildMimeMessage } from '@/tools/gmail/utils'
8+
import {
9+
base64UrlEncode,
10+
buildMimeMessage,
11+
buildSimpleEmailMessage,
12+
fetchThreadingHeaders,
13+
} from '@/tools/gmail/utils'
914

1015
export const dynamic = 'force-dynamic'
1116

@@ -16,8 +21,10 @@ const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
1621
const GmailSendSchema = z.object({
1722
accessToken: z.string().min(1, 'Access token is required'),
1823
to: z.string().min(1, 'Recipient email is required'),
19-
subject: z.string().min(1, 'Subject is required'),
24+
subject: z.string().optional().nullable(),
2025
body: z.string().min(1, 'Email body is required'),
26+
threadId: z.string().optional().nullable(),
27+
replyToMessageId: z.string().optional().nullable(),
2128
cc: z.string().optional().nullable(),
2229
bcc: z.string().optional().nullable(),
2330
attachments: z.array(z.any()).optional().nullable(),
@@ -49,11 +56,19 @@ export async function POST(request: NextRequest) {
4956

5057
logger.info(`[${requestId}] Sending Gmail email`, {
5158
to: validatedData.to,
52-
subject: validatedData.subject,
59+
subject: validatedData.subject || '',
5360
hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0),
5461
attachmentCount: validatedData.attachments?.length || 0,
5562
})
5663

64+
const threadingHeaders = validatedData.replyToMessageId
65+
? await fetchThreadingHeaders(validatedData.replyToMessageId, validatedData.accessToken)
66+
: {}
67+
68+
const originalMessageId = threadingHeaders.messageId
69+
const originalReferences = threadingHeaders.references
70+
const originalSubject = threadingHeaders.subject
71+
5772
let rawMessage: string | undefined
5873

5974
if (validatedData.attachments && validatedData.attachments.length > 0) {
@@ -106,8 +121,10 @@ export async function POST(request: NextRequest) {
106121
to: validatedData.to,
107122
cc: validatedData.cc ?? undefined,
108123
bcc: validatedData.bcc ?? undefined,
109-
subject: validatedData.subject,
124+
subject: validatedData.subject || originalSubject || '',
110125
body: validatedData.body,
126+
inReplyTo: originalMessageId,
127+
references: originalReferences,
111128
attachments: attachmentBuffers,
112129
})
113130

@@ -117,22 +134,21 @@ export async function POST(request: NextRequest) {
117134
}
118135

119136
if (!rawMessage) {
120-
const emailHeaders = [
121-
'Content-Type: text/plain; charset="UTF-8"',
122-
'MIME-Version: 1.0',
123-
`To: ${validatedData.to}`,
124-
]
125-
126-
if (validatedData.cc) {
127-
emailHeaders.push(`Cc: ${validatedData.cc}`)
128-
}
129-
if (validatedData.bcc) {
130-
emailHeaders.push(`Bcc: ${validatedData.bcc}`)
131-
}
137+
rawMessage = buildSimpleEmailMessage({
138+
to: validatedData.to,
139+
cc: validatedData.cc,
140+
bcc: validatedData.bcc,
141+
subject: validatedData.subject || originalSubject,
142+
body: validatedData.body,
143+
inReplyTo: originalMessageId,
144+
references: originalReferences,
145+
})
146+
}
147+
148+
const requestBody: { raw: string; threadId?: string } = { raw: rawMessage }
132149

133-
emailHeaders.push(`Subject: ${validatedData.subject}`, '', validatedData.body)
134-
const email = emailHeaders.join('\n')
135-
rawMessage = Buffer.from(email).toString('base64url')
150+
if (validatedData.threadId) {
151+
requestBody.threadId = validatedData.threadId
136152
}
137153

138154
const gmailResponse = await fetch(`${GMAIL_API_BASE}/messages/send`, {
@@ -141,7 +157,7 @@ export async function POST(request: NextRequest) {
141157
Authorization: `Bearer ${validatedData.accessToken}`,
142158
'Content-Type': 'application/json',
143159
},
144-
body: JSON.stringify({ raw: rawMessage }),
160+
body: JSON.stringify(requestBody),
145161
})
146162

147163
if (!gmailResponse.ok) {

apps/sim/blocks/blocks/gmail.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
6565
layout: 'full',
6666
placeholder: 'Email subject',
6767
condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] },
68-
required: true,
68+
required: false,
6969
},
7070
{
7171
id: 'body',
@@ -101,6 +101,27 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
101101
mode: 'advanced',
102102
required: false,
103103
},
104+
// Advanced Settings - Threading
105+
{
106+
id: 'threadId',
107+
title: 'Thread ID',
108+
type: 'short-input',
109+
layout: 'full',
110+
placeholder: 'Thread ID to reply to (for threading)',
111+
condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] },
112+
mode: 'advanced',
113+
required: false,
114+
},
115+
{
116+
id: 'replyToMessageId',
117+
title: 'Reply to Message ID',
118+
type: 'short-input',
119+
layout: 'full',
120+
placeholder: 'Gmail message ID (not RFC Message-ID) - use the "id" field from results',
121+
condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] },
122+
mode: 'advanced',
123+
required: false,
124+
},
104125
// Advanced Settings - Additional Recipients
105126
{
106127
id: 'cc',
@@ -241,13 +262,18 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
241262
to: { type: 'string', description: 'Recipient email address' },
242263
subject: { type: 'string', description: 'Email subject' },
243264
body: { type: 'string', description: 'Email content' },
265+
threadId: { type: 'string', description: 'Thread ID to reply to (for threading)' },
266+
replyToMessageId: {
267+
type: 'string',
268+
description: 'Gmail message ID to reply to (use "id" field from results, not "messageId")',
269+
},
244270
cc: { type: 'string', description: 'CC recipients (comma-separated)' },
245271
bcc: { type: 'string', description: 'BCC recipients (comma-separated)' },
246272
attachments: { type: 'json', description: 'Files to attach (UserFile array)' },
247273
// Read operation inputs
248274
folder: { type: 'string', description: 'Gmail folder' },
249275
manualFolder: { type: 'string', description: 'Manual folder name' },
250-
messageId: { type: 'string', description: 'Message identifier' },
276+
readMessageId: { type: 'string', description: 'Message identifier for reading specific email' },
251277
unreadOnly: { type: 'boolean', description: 'Unread messages only' },
252278
includeAttachments: { type: 'boolean', description: 'Include email attachments' },
253279
// Search operation inputs

apps/sim/lib/webhooks/outlook-polling-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ async function processOutlookEmails(
368368
const simplifiedEmail: SimplifiedOutlookEmail = {
369369
id: email.id,
370370
conversationId: email.conversationId,
371-
subject: email.subject || '(No Subject)',
371+
subject: email.subject || '',
372372
from: email.from?.emailAddress?.address || '',
373373
to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
374374
cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '',

apps/sim/tools/gmail/draft.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
2828
},
2929
subject: {
3030
type: 'string',
31-
required: true,
31+
required: false,
3232
visibility: 'user-or-llm',
3333
description: 'Email subject',
3434
},
@@ -38,6 +38,19 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
3838
visibility: 'user-or-llm',
3939
description: 'Email body content',
4040
},
41+
threadId: {
42+
type: 'string',
43+
required: false,
44+
visibility: 'user-or-llm',
45+
description: 'Thread ID to reply to (for threading)',
46+
},
47+
replyToMessageId: {
48+
type: 'string',
49+
required: false,
50+
visibility: 'user-or-llm',
51+
description:
52+
'Gmail message ID to reply to - use the "id" field from Gmail Read results (not the RFC "messageId")',
53+
},
4154
cc: {
4255
type: 'string',
4356
required: false,
@@ -69,6 +82,8 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
6982
to: params.to,
7083
subject: params.subject,
7184
body: params.body,
85+
threadId: params.threadId,
86+
replyToMessageId: params.replyToMessageId,
7287
cc: params.cc,
7388
bcc: params.bcc,
7489
attachments: params.attachments,

apps/sim/tools/gmail/read.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
225225
threadId: msg.threadId,
226226
subject: msg.subject,
227227
from: msg.from,
228+
to: msg.to,
228229
date: msg.date,
229230
})),
230231
},

apps/sim/tools/gmail/send.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
2828
},
2929
subject: {
3030
type: 'string',
31-
required: true,
31+
required: false,
3232
visibility: 'user-or-llm',
3333
description: 'Email subject',
3434
},
@@ -38,6 +38,19 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
3838
visibility: 'user-or-llm',
3939
description: 'Email body content',
4040
},
41+
threadId: {
42+
type: 'string',
43+
required: false,
44+
visibility: 'user-or-llm',
45+
description: 'Thread ID to reply to (for threading)',
46+
},
47+
replyToMessageId: {
48+
type: 'string',
49+
required: false,
50+
visibility: 'user-or-llm',
51+
description:
52+
'Gmail message ID to reply to - use the "id" field from Gmail Read results (not the RFC "messageId")',
53+
},
4154
cc: {
4255
type: 'string',
4356
required: false,
@@ -69,6 +82,8 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
6982
to: params.to,
7083
subject: params.subject,
7184
body: params.body,
85+
threadId: params.threadId,
86+
replyToMessageId: params.replyToMessageId,
7287
cc: params.cc,
7388
bcc: params.bcc,
7489
attachments: params.attachments,

apps/sim/tools/gmail/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ export interface GmailSendParams extends BaseGmailParams {
1111
to: string
1212
cc?: string
1313
bcc?: string
14-
subject: string
14+
subject?: string
1515
body: string
16+
threadId?: string
17+
replyToMessageId?: string
1618
attachments?: UserFile[]
1719
}
1820

0 commit comments

Comments
 (0)