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
119 changes: 119 additions & 0 deletions apps/sim/tools/gmail/create_label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'

interface GmailCreateLabelParams {
accessToken: string
name: string
messageListVisibility?: string
labelListVisibility?: string
}

interface GmailCreateLabelResponse {
success: boolean
output: {
id: string
name: string
messageListVisibility?: string
labelListVisibility?: string
type?: string
}
}

export const gmailCreateLabelV2Tool: ToolConfig<GmailCreateLabelParams, GmailCreateLabelResponse> =
{
id: 'gmail_create_label_v2',
name: 'Gmail Create Label',
description: 'Create a new label in Gmail',
version: '2.0.0',

oauth: {
required: true,
provider: 'google-email',
},

params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Gmail API',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Display name for the new label',
},
messageListVisibility: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Visibility of messages with this label in the message list (show or hide)',
},
labelListVisibility: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Visibility of the label in the label list (labelShow, labelShowIfUnread, or labelHide)',
},
},

request: {
url: () => `${GMAIL_API_BASE}/labels`,
method: 'POST',
headers: (params: GmailCreateLabelParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GmailCreateLabelParams) => {
const body: Record<string, string> = { name: params.name }
if (params.messageListVisibility) {
body.messageListVisibility = params.messageListVisibility
}
if (params.labelListVisibility) {
body.labelListVisibility = params.labelListVisibility
}
return body
},
},

transformResponse: async (response: Response) => {
const data = await response.json()

if (!response.ok) {
return {
success: false,
output: { id: '', name: '' },
error: data.error?.message || 'Failed to create label',
}
}

return {
success: true,
output: {
id: data.id,
name: data.name,
messageListVisibility: data.messageListVisibility ?? null,
labelListVisibility: data.labelListVisibility ?? null,
type: data.type ?? null,
},
}
},

outputs: {
id: { type: 'string', description: 'Label ID' },
name: { type: 'string', description: 'Label display name' },
messageListVisibility: {
type: 'string',
description: 'Visibility of messages with this label',
optional: true,
},
labelListVisibility: {
type: 'string',
description: 'Visibility of the label in the label list',
optional: true,
},
type: { type: 'string', description: 'Label type (system or user)', optional: true },
},
}
76 changes: 76 additions & 0 deletions apps/sim/tools/gmail/delete_draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'

interface GmailDeleteDraftParams {
accessToken: string
draftId: string
}

interface GmailDeleteDraftResponse {
success: boolean
output: {
deleted: boolean
draftId: string
}
}

export const gmailDeleteDraftV2Tool: ToolConfig<GmailDeleteDraftParams, GmailDeleteDraftResponse> =
{
id: 'gmail_delete_draft_v2',
name: 'Gmail Delete Draft',
description: 'Delete a specific draft from Gmail',
version: '2.0.0',

oauth: {
required: true,
provider: 'google-email',
},

params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Gmail API',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to delete',
},
},

request: {
url: (params: GmailDeleteDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}`,
method: 'DELETE',
headers: (params: GmailDeleteDraftParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},

transformResponse: async (response: Response, params?: GmailDeleteDraftParams) => {
if (!response.ok) {
const data = await response.json()
return {
success: false,
output: { deleted: false, draftId: params?.draftId ?? '' },
error: data.error?.message || 'Failed to delete draft',
}
}

return {
success: true,
output: {
deleted: true,
draftId: params?.draftId ?? '',
},
}
},

outputs: {
deleted: { type: 'boolean', description: 'Whether the draft was successfully deleted' },
draftId: { type: 'string', description: 'ID of the deleted draft' },
},
}
76 changes: 76 additions & 0 deletions apps/sim/tools/gmail/delete_label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'

interface GmailDeleteLabelParams {
accessToken: string
labelId: string
}

interface GmailDeleteLabelResponse {
success: boolean
output: {
deleted: boolean
labelId: string
}
}

export const gmailDeleteLabelV2Tool: ToolConfig<GmailDeleteLabelParams, GmailDeleteLabelResponse> =
{
id: 'gmail_delete_label_v2',
name: 'Gmail Delete Label',
description: 'Delete a label from Gmail',
version: '2.0.0',

oauth: {
required: true,
provider: 'google-email',
},

params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Gmail API',
},
labelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the label to delete',
},
},

request: {
url: (params: GmailDeleteLabelParams) => `${GMAIL_API_BASE}/labels/${params.labelId}`,
method: 'DELETE',
headers: (params: GmailDeleteLabelParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},

transformResponse: async (response: Response, params?: GmailDeleteLabelParams) => {
if (!response.ok) {
const data = await response.json()
return {
success: false,
output: { deleted: false, labelId: params?.labelId ?? '' },
error: data.error?.message || 'Failed to delete label',
}
}

return {
success: true,
output: {
deleted: true,
labelId: params?.labelId ?? '',
},
}
},

outputs: {
deleted: { type: 'boolean', description: 'Whether the label was successfully deleted' },
labelId: { type: 'string', description: 'ID of the deleted label' },
},
}
117 changes: 117 additions & 0 deletions apps/sim/tools/gmail/get_draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
import type { ToolConfig } from '@/tools/types'

interface GmailGetDraftParams {
accessToken: string
draftId: string
}

interface GmailGetDraftResponse {
success: boolean
output: {
id: string
messageId?: string
threadId?: string
to?: string
from?: string
subject?: string
body?: string
labelIds?: string[]
}
}

export const gmailGetDraftV2Tool: ToolConfig<GmailGetDraftParams, GmailGetDraftResponse> = {
id: 'gmail_get_draft_v2',
name: 'Gmail Get Draft',
description: 'Get a specific draft from Gmail by its ID',
version: '2.0.0',

oauth: {
required: true,
provider: 'google-email',
},

params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Gmail API',
},
draftId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the draft to retrieve',
},
},

request: {
url: (params: GmailGetDraftParams) => `${GMAIL_API_BASE}/drafts/${params.draftId}?format=full`,
method: 'GET',
headers: (params: GmailGetDraftParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},

transformResponse: async (response: Response) => {
const data = await response.json()

if (!response.ok) {
return {
success: false,
output: { id: '' },
error: data.error?.message || 'Failed to get draft',
}
}

const message = data.message || {}
const headers = message.payload?.headers || []
const getHeader = (name: string): string | undefined =>
headers.find((h: Record<string, string>) => h.name.toLowerCase() === name.toLowerCase())
?.value

let body = ''
if (message.payload?.body?.data) {
body = Buffer.from(message.payload.body.data, 'base64').toString()
} else if (message.payload?.parts) {
const textPart = message.payload.parts.find(
(part: Record<string, unknown>) => part.mimeType === 'text/plain'
)
if (textPart?.body?.data) {
body = Buffer.from(textPart.body.data, 'base64').toString()
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Body extraction misses HTML-only and nested multipart emails

Medium Severity

The inline body extraction logic in get_draft.ts and get_thread.ts only handles direct body data and text/plain MIME parts. It misses text/html fallback and recursive nested multipart structures, returning an empty body for HTML-only emails. The existing exported extractMessageBody utility in utils.ts already handles all these cases correctly and is used by other Gmail tools like read.

Additional Locations (1)

Fix in Cursor Fix in Web


return {
success: true,
output: {
id: data.id,
messageId: message.id ?? undefined,
threadId: message.threadId ?? undefined,
to: getHeader('To'),
from: getHeader('From'),
subject: getHeader('Subject'),
body: body || undefined,
labelIds: message.labelIds ?? undefined,
},
}
},

outputs: {
id: { type: 'string', description: 'Draft ID' },
messageId: { type: 'string', description: 'Gmail message ID', optional: true },
threadId: { type: 'string', description: 'Gmail thread ID', optional: true },
to: { type: 'string', description: 'Recipient email address', optional: true },
from: { type: 'string', description: 'Sender email address', optional: true },
subject: { type: 'string', description: 'Draft subject', optional: true },
body: { type: 'string', description: 'Draft body text', optional: true },
labelIds: {
type: 'array',
items: { type: 'string' },
description: 'Draft labels',
optional: true,
},
},
}
Loading