Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/docs/content/docs/en/tools/gmail.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,34 @@ Draft emails using Gmail. Returns API-aligned fields only.
| `threadId` | string | Gmail thread ID |
| `labelIds` | array | Email labels |

Comment thread
waleedlatif1 marked this conversation as resolved.
### `gmail_edit_draft`

Update an existing Gmail draft in place without deleting and recreating it.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `draftId` | string | Yes | ID of the draft to update \(from Gmail List Drafts or Gmail Get Draft\) |
| `to` | string | Yes | Recipient email address |
| `subject` | string | No | Email subject |
| `body` | string | Yes | Email body content |
| `contentType` | string | No | Content type for the email body \(text or html\) |
| `threadId` | string | No | Thread ID to associate the draft with \(for threading\) |
| `replyToMessageId` | string | No | Gmail message ID to reply to - use the "id" field from Gmail Read results \(not the RFC "messageId"\) |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
| `attachments` | file[] | No | Files to attach to the email draft |

#### Output

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `draftId` | string | Draft ID |
| `messageId` | string | Gmail message ID for the draft |
| `threadId` | string | Gmail thread ID |
| `labelIds` | array | Email labels |

### `gmail_read`

Read emails from Gmail. Returns API-aligned fields only.
Expand Down
1 change: 0 additions & 1 deletion apps/docs/content/docs/en/tools/outlook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Send emails using Outlook
| `body` | string | Yes | Email body content |
| `contentType` | string | No | Content type for the email body \(text or html\) |
| `replyToMessageId` | string | No | Message ID to reply to \(for threading\) |
| `conversationId` | string | No | Conversation ID for threading |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
| `attachments` | file[] | No | Files to attach to the email |
Expand Down
466 changes: 0 additions & 466 deletions apps/docs/content/docs/en/triggers/linear.mdx

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4662,6 +4662,10 @@
"name": "Draft Email",
"description": "Draft emails using Gmail"
},
{
"name": "Edit Draft",
"description": "Update an existing Gmail draft in place without deleting and recreating it."
},
{
"name": "Search Email",
"description": "Search emails in Gmail"
Expand Down Expand Up @@ -4699,7 +4703,7 @@
"description": "Remove label(s) from a Gmail message"
}
],
"operationCount": 12,
"operationCount": 13,
"triggers": [
{
"id": "gmail_poller",
Expand Down
180 changes: 180 additions & 0 deletions apps/sim/app/api/tools/gmail/edit-draft/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { gmailEditDraftContract } from '@/lib/api/contracts/google-tools'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import {
base64UrlEncode,
buildMimeMessage,
buildSimpleEmailMessage,
fetchThreadingHeaders,
GMAIL_API_BASE,
} from '@/tools/gmail/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('GmailEditDraftAPI')

export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()

try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Gmail edit draft attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}

logger.info(
`[${requestId}] Authenticated Gmail edit draft request via ${authResult.authType}`,
{ userId: authResult.userId }
)

const parsed = await parseRequest(gmailEditDraftContract, request, {})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body

logger.info(`[${requestId}] Updating Gmail draft`, {
draftId: validatedData.draftId,
to: validatedData.to,
hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0),
attachmentCount: validatedData.attachments?.length || 0,
})

const threadingHeaders = validatedData.replyToMessageId
? await fetchThreadingHeaders(validatedData.replyToMessageId, validatedData.accessToken)
: {}

const originalMessageId = threadingHeaders.messageId
const originalReferences = threadingHeaders.references
const originalSubject = threadingHeaders.subject

let rawMessage: string | undefined

if (validatedData.attachments && validatedData.attachments.length > 0) {
const rawAttachments = validatedData.attachments
const attachments = processFilesToUserFiles(rawAttachments, requestId, logger)

if (attachments.length > 0) {
const totalSize = attachments.reduce((sum, file) => sum + file.size, 0)
const maxSize = 25 * 1024 * 1024

if (totalSize > maxSize) {
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2)
return NextResponse.json(
{
success: false,
error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`,
},
{ status: 400 }
)
}

const attachmentBuffers = await Promise.all(
attachments.map(async (file) => {
const buffer = await downloadFileFromStorage(file, requestId, logger)
return {
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffer,
}
})
)

const mimeMessage = buildMimeMessage({
to: validatedData.to,
cc: validatedData.cc ?? undefined,
bcc: validatedData.bcc ?? undefined,
subject: validatedData.subject || originalSubject || '',
body: validatedData.body,
contentType: validatedData.contentType || 'text',
inReplyTo: originalMessageId,
references: originalReferences,
attachments: attachmentBuffers,
})

rawMessage = base64UrlEncode(mimeMessage)
}
}

if (!rawMessage) {
rawMessage = buildSimpleEmailMessage({
to: validatedData.to,
cc: validatedData.cc,
bcc: validatedData.bcc,
subject: validatedData.subject || originalSubject,
body: validatedData.body,
contentType: validatedData.contentType || 'text',
inReplyTo: originalMessageId,
references: originalReferences,
})
}

const draftMessage: { raw: string; threadId?: string } = { raw: rawMessage }
if (validatedData.threadId) {
draftMessage.threadId = validatedData.threadId
}

const gmailResponse = await fetch(
`${GMAIL_API_BASE}/drafts/${encodeURIComponent(validatedData.draftId)}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: validatedData.draftId,
message: draftMessage,
}),
}
)

if (!gmailResponse.ok) {
const errorText = await gmailResponse.text()
logger.error(`[${requestId}] Gmail API error:`, errorText)
return NextResponse.json(
{
success: false,
error: `Gmail API error: ${gmailResponse.statusText}`,
},
{ status: gmailResponse.status }
)
}

const data = await gmailResponse.json()

logger.info(`[${requestId}] Draft updated successfully`, { draftId: data.id })

return NextResponse.json({
success: true,
output: {
draftId: data.id ?? null,
messageId: data.message?.id ?? null,
threadId: data.message?.threadId ?? null,
labelIds: data.message?.labelIds ?? null,
},
})
} catch (error) {
logger.error(`[${requestId}] Error updating Gmail draft:`, error)

return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
)
}
})
Loading
Loading