import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import nodemailer from 'nodemailer' import { smtpSendContract } from '@/lib/api/contracts/tools/communication/email' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateDatabaseHost } from '@/lib/core/security/input-validation.server' 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 { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' const logger = createLogger('SmtpSendAPI') export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required', }, { status: 401 } ) } const userId = authResult.userId logger.info(`[${requestId}] Authenticated SMTP request via ${authResult.authType}`, { userId, }) const parsed = await parseRequest(smtpSendContract, request, {}) if (!parsed.success) return parsed.response const validatedData = parsed.data.body const hostValidation = await validateDatabaseHost(validatedData.smtpHost, 'smtpHost') if (!hostValidation.isValid) { logger.warn(`[${requestId}] SMTP host validation failed`, { host: validatedData.smtpHost, error: hostValidation.error, }) return NextResponse.json({ success: false, error: hostValidation.error }, { status: 400 }) } logger.info(`[${requestId}] Sending email via SMTP`, { host: validatedData.smtpHost, port: validatedData.smtpPort, to: validatedData.to, subject: validatedData.subject, secure: validatedData.smtpSecure, }) // Pin the pre-resolved IP to prevent DNS rebinding (TOCTOU) attacks. // Pass resolvedIP as the host so nodemailer connects to the validated address, // and set servername for correct TLS SNI/certificate validation. const pinnedHost = hostValidation.resolvedIP ?? validatedData.smtpHost const transporter = nodemailer.createTransport({ host: pinnedHost, port: validatedData.smtpPort, secure: validatedData.smtpSecure === 'SSL', auth: { user: validatedData.smtpUsername, pass: validatedData.smtpPassword, }, tls: validatedData.smtpSecure === 'None' ? { rejectUnauthorized: false, servername: validatedData.smtpHost } : { rejectUnauthorized: true, servername: validatedData.smtpHost }, }) const contentType = validatedData.contentType || 'text' const fromAddress = validatedData.fromName ? `"${validatedData.fromName}" <${validatedData.from}>` : validatedData.from const mailOptions: nodemailer.SendMailOptions = { from: fromAddress, to: validatedData.to, subject: validatedData.subject, [contentType === 'html' ? 'html' : 'text']: validatedData.body, } if (validatedData.cc) { mailOptions.cc = validatedData.cc } if (validatedData.bcc) { mailOptions.bcc = validatedData.bcc } if (validatedData.replyTo) { mailOptions.replyTo = validatedData.replyTo } if (validatedData.attachments && validatedData.attachments.length > 0) { const rawAttachments = validatedData.attachments logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) 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 SMTP limit of 25MB`, }, { status: 400 } ) } const accessResults = await Promise.all( attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) ) const denied = accessResults.find((r) => r !== null) if (denied) return denied let resolved: Array<{ buffer: Buffer; contentType: string }> try { resolved = await Promise.all( attachments.map(async (file) => { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) return await downloadServableFileFromStorage(file, requestId, logger) }) ) } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady logger.error(`[${requestId}] Failed to download an attachment:`, error) return NextResponse.json( { success: false, error: `Failed to download attachment: ${getErrorMessage(error, 'Unknown error')}`, }, { status: 500 } ) } const resolvedTotal = resolved.reduce((sum, r) => sum + r.buffer.length, 0) if (resolvedTotal > maxSize) { const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2) return NextResponse.json( { success: false, error: `Total attachment size (${sizeMB}MB) exceeds SMTP limit of 25MB`, }, { status: 400 } ) } const attachmentBuffers = attachments.map((file, i) => ({ filename: file.name, content: resolved[i].buffer, contentType: resolved[i].contentType || file.type || 'application/octet-stream', })) logger.info(`[${requestId}] Processed ${attachmentBuffers.length} attachment(s)`) mailOptions.attachments = attachmentBuffers } } const result = await transporter.sendMail(mailOptions) logger.info(`[${requestId}] Email sent successfully via SMTP`, { messageId: result.messageId, to: validatedData.to, }) return NextResponse.json({ success: true, messageId: result.messageId, to: validatedData.to, subject: validatedData.subject, }) } catch (error: unknown) { // Type guard for error objects with code property const isNodeError = (err: unknown): err is NodeJS.ErrnoException => { return err instanceof Error && 'code' in err } let errorMessage = 'Failed to send email via SMTP' if (isNodeError(error)) { if (error.code === 'EAUTH') { errorMessage = 'SMTP authentication failed - check username and password' } else if ( error.code === 'ECONNECTION' || error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' ) { errorMessage = 'Could not connect to SMTP server - check host and port' } } const hasResponseCode = (err: unknown): err is { responseCode: number } => { return typeof err === 'object' && err !== null && 'responseCode' in err } if (hasResponseCode(error)) { if (error.responseCode >= 500) { errorMessage = 'SMTP server error - please try again later' } else if (error.responseCode >= 400) { errorMessage = 'Email rejected by SMTP server - check recipient addresses' } } logger.error(`[${requestId}] Error sending email via SMTP:`, { error: toError(error).message, code: isNodeError(error) ? error.code : undefined, responseCode: hasResponseCode(error) ? error.responseCode : undefined, }) return NextResponse.json( { success: false, error: errorMessage, }, { status: 500 } ) } })