From 2d96ac55db1ebf6f8297126947f042d7e7655bb9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 3 Feb 2026 15:44:39 -0800 Subject: [PATCH] fix sendgrid --- .../app/api/tools/sendgrid/send-mail/route.ts | 188 ++++++++++++++++++ apps/sim/tools/sendgrid/send_mail.ts | 128 ++++-------- 2 files changed, 224 insertions(+), 92 deletions(-) create mode 100644 apps/sim/app/api/tools/sendgrid/send-mail/route.ts diff --git a/apps/sim/app/api/tools/sendgrid/send-mail/route.ts b/apps/sim/app/api/tools/sendgrid/send-mail/route.ts new file mode 100644 index 000000000..362960b89 --- /dev/null +++ b/apps/sim/app/api/tools/sendgrid/send-mail/route.ts @@ -0,0 +1,188 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' +import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SendGridSendMailAPI') + +const SendGridSendMailSchema = z.object({ + apiKey: z.string().min(1, 'API key is required'), + from: z.string().min(1, 'From email is required'), + fromName: z.string().optional().nullable(), + to: z.string().min(1, 'To email is required'), + toName: z.string().optional().nullable(), + subject: z.string().optional().nullable(), + content: z.string().optional().nullable(), + contentType: z.string().optional().nullable(), + cc: z.string().optional().nullable(), + bcc: z.string().optional().nullable(), + replyTo: z.string().optional().nullable(), + replyToName: z.string().optional().nullable(), + templateId: z.string().optional().nullable(), + dynamicTemplateData: z.any().optional().nullable(), + attachments: RawFileInputArraySchema.optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized SendGrid send attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated SendGrid send request via ${authResult.authType}`) + + const body = await request.json() + const validatedData = SendGridSendMailSchema.parse(body) + + logger.info(`[${requestId}] Sending SendGrid email`, { + to: validatedData.to, + subject: validatedData.subject || '(template)', + hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0), + attachmentCount: validatedData.attachments?.length || 0, + }) + + // Build personalizations + const personalizations: Record = { + to: [ + { email: validatedData.to, ...(validatedData.toName && { name: validatedData.toName }) }, + ], + } + + if (validatedData.cc) { + personalizations.cc = [{ email: validatedData.cc }] + } + + if (validatedData.bcc) { + personalizations.bcc = [{ email: validatedData.bcc }] + } + + if (validatedData.templateId && validatedData.dynamicTemplateData) { + personalizations.dynamic_template_data = + typeof validatedData.dynamicTemplateData === 'string' + ? JSON.parse(validatedData.dynamicTemplateData) + : validatedData.dynamicTemplateData + } + + // Build mail body + const mailBody: Record = { + personalizations: [personalizations], + from: { + email: validatedData.from, + ...(validatedData.fromName && { name: validatedData.fromName }), + }, + subject: validatedData.subject, + } + + if (validatedData.templateId) { + mailBody.template_id = validatedData.templateId + } else { + mailBody.content = [ + { + type: validatedData.contentType || 'text/plain', + value: validatedData.content, + }, + ] + } + + if (validatedData.replyTo) { + mailBody.reply_to = { + email: validatedData.replyTo, + ...(validatedData.replyToName && { name: validatedData.replyToName }), + } + } + + // Process attachments from UserFile objects + if (validatedData.attachments && validatedData.attachments.length > 0) { + const rawAttachments = validatedData.attachments + logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) + + const userFiles = processFilesToUserFiles(rawAttachments, requestId, logger) + + if (userFiles.length > 0) { + const sendGridAttachments = await Promise.all( + userFiles.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + const buffer = await downloadFileFromStorage(file, requestId, logger) + + return { + content: buffer.toString('base64'), + filename: file.name, + type: file.type || 'application/octet-stream', + disposition: 'attachment', + } + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + mailBody.attachments = sendGridAttachments + } + } + + // Send to SendGrid + const response = await fetch('https://api.sendgrid.com/v3/mail/send', { + method: 'POST', + headers: { + Authorization: `Bearer ${validatedData.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mailBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const errorMessage = + errorData.errors?.[0]?.message || errorData.message || 'Failed to send email' + logger.error(`[${requestId}] SendGrid API error:`, { status: response.status, errorData }) + return NextResponse.json({ success: false, error: errorMessage }, { status: response.status }) + } + + const messageId = response.headers.get('X-Message-Id') + logger.info(`[${requestId}] Email sent successfully`, { messageId }) + + return NextResponse.json({ + success: true, + output: { + success: true, + messageId: messageId || undefined, + to: validatedData.to, + subject: validatedData.subject || '', + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Validation error:`, error.errors) + return NextResponse.json( + { success: false, error: error.errors[0]?.message || 'Validation failed' }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Unexpected error:`, error) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/tools/sendgrid/send_mail.ts b/apps/sim/tools/sendgrid/send_mail.ts index 44f493228..19b57ae98 100644 --- a/apps/sim/tools/sendgrid/send_mail.ts +++ b/apps/sim/tools/sendgrid/send_mail.ts @@ -1,9 +1,4 @@ -import type { - SendGridMailBody, - SendGridPersonalization, - SendMailParams, - SendMailResult, -} from '@/tools/sendgrid/types' +import type { SendMailParams, SendMailResult } from '@/tools/sendgrid/types' import type { ToolConfig } from '@/tools/types' export const sendGridSendMailTool: ToolConfig = { @@ -89,7 +84,7 @@ export const sendGridSendMailTool: ToolConfig = type: 'file[]', required: false, visibility: 'user-or-llm', - description: 'Files to attach to the email as an array of attachment objects', + description: 'Files to attach to the email (UserFile objects)', }, templateId: { type: 'string', @@ -106,100 +101,49 @@ export const sendGridSendMailTool: ToolConfig = }, request: { - url: () => 'https://api.sendgrid.com/v3/mail/send', + url: '/api/tools/sendgrid/send-mail', method: 'POST', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + headers: () => ({ 'Content-Type': 'application/json', }), - body: (params) => { - const personalizations: SendGridPersonalization = { - to: [ - { - email: params.to, - ...(params.toName && { name: params.toName }), - }, - ], - } - - if (params.cc) { - personalizations.cc = [{ email: params.cc }] - } - - if (params.bcc) { - personalizations.bcc = [{ email: params.bcc }] - } - - if (params.templateId && params.dynamicTemplateData) { - try { - personalizations.dynamic_template_data = - typeof params.dynamicTemplateData === 'string' - ? JSON.parse(params.dynamicTemplateData) - : params.dynamicTemplateData - } catch (e) { - // If parsing fails, use as-is - } - } - - const mailBody: SendGridMailBody = { - personalizations: [personalizations], - from: { - email: params.from, - ...(params.fromName && { name: params.fromName }), - }, - subject: params.subject, - } - - if (params.templateId) { - mailBody.template_id = params.templateId - } else { - mailBody.content = [ - { - type: params.contentType || 'text/plain', - value: params.content, - }, - ] - } - - if (params.replyTo) { - mailBody.reply_to = { - email: params.replyTo, - ...(params.replyToName && { name: params.replyToName }), - } - } - - if (params.attachments) { - try { - mailBody.attachments = - typeof params.attachments === 'string' - ? JSON.parse(params.attachments) - : params.attachments - } catch (e) { - // If parsing fails, skip attachments - } - } - - return { body: JSON.stringify(mailBody) } - }, + body: (params) => ({ + apiKey: params.apiKey, + from: params.from, + fromName: params.fromName, + to: params.to, + toName: params.toName, + subject: params.subject, + content: params.content, + contentType: params.contentType, + cc: params.cc, + bcc: params.bcc, + replyTo: params.replyTo, + replyToName: params.replyToName, + templateId: params.templateId, + dynamicTemplateData: params.dynamicTemplateData, + attachments: params.attachments, + }), }, - transformResponse: async (response, params): Promise => { - if (!response.ok) { - const error = await response.json() - throw new Error(error.errors?.[0]?.message || 'Failed to send email') - } + transformResponse: async (response): Promise => { + const data = await response.json() - // SendGrid returns 202 Accepted with X-Message-Id header - const messageId = response.headers.get('X-Message-Id') + if (!data.success) { + return { + success: false, + output: { + success: false, + messageId: undefined, + to: '', + subject: '', + }, + error: data.error || 'Failed to send email', + } + } return { success: true, - output: { - success: true, - messageId: messageId || undefined, - to: params?.to || '', - subject: params?.subject || '', - }, + output: data.output, } },