fix sendgrid

This commit is contained in:
Vikhyath Mondreti
2026-02-03 15:44:39 -08:00
parent bd5866ed6b
commit 2d96ac55db
2 changed files with 224 additions and 92 deletions

View File

@@ -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<string, unknown> = {
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<string, unknown> = {
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 }
)
}
}

View File

@@ -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<SendMailParams, SendMailResult> = {
@@ -89,7 +84,7 @@ export const sendGridSendMailTool: ToolConfig<SendMailParams, SendMailResult> =
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<SendMailParams, SendMailResult> =
},
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<SendMailResult> => {
if (!response.ok) {
const error = await response.json()
throw new Error(error.errors?.[0]?.message || 'Failed to send email')
}
transformResponse: async (response): Promise<SendMailResult> => {
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,
}
},