import { getLogger } from "../shared/config"; import { getCorsHandler } from "../shared/middleware"; import { onRequest } from "firebase-functions/v2/https"; import { Request } from "firebase-functions/v2/https"; import { Response } from "express"; import { SESClient } from "@aws-sdk/client-ses"; import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; import { HttpsError } from "firebase-functions/v2/https"; import * as mime from 'mime-types'; import axios from 'axios'; const logger = getLogger(); const corsHandler = getCorsHandler(); interface EmailRequest { to: string | string[]; subject: string; html: string; text?: string; from: string; replyTo?: string; attachments?: Attachment[]; fileUrl?: string; fileName?: string; } interface Attachment { filename: string; content: string | Buffer; // Base64 encoded string or Buffer contentType?: string; } const stripHtml = (html: string): string => { if (!html) return ''; return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); } async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ region: 'ap-south-1', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' } }); const command = new SendEmailCommand({ Source: data.from, Destination: { ToAddresses: recipients }, Message: { Subject: { Data: data.subject }, Body: { Html: { Data: data.html }, Text: { Data: data.text || stripHtml(data.html) } } }, ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined, }); const result = await ses.send(command); return { messageId: result.MessageId }; } async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { const ses = new SESClient({ region: 'ap-south-1', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' } }); const boundary = `boundary_${Math.random().toString(16).substr(2)}`; let rawMessage = `From: ${data.from}\n`; rawMessage += `To: ${recipients.join(', ')}\n`; rawMessage += `Subject: ${data.subject}\n`; rawMessage += `MIME-Version: 1.0\n`; rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; // Add email body (multipart/alternative) rawMessage += `--${boundary}\n`; rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; // Text part if (data.text) { rawMessage += `--alt_${boundary}\n`; rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; rawMessage += `${data.text}\n\n`; } // HTML part rawMessage += `--alt_${boundary}\n`; rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; rawMessage += `${data.html}\n\n`; // Close alternative part rawMessage += `--alt_${boundary}--\n\n`; // Add attachments for (const attachment of data.attachments || []) { const contentType = attachment.contentType || mime.lookup(attachment.filename) || 'application/octet-stream'; rawMessage += `--${boundary}\n`; rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`; rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; rawMessage += `Content-Transfer-Encoding: base64\n\n`; const contentBuffer = typeof attachment.content === 'string' ? Buffer.from(attachment.content, 'base64') : attachment.content; rawMessage += contentBuffer.toString('base64') + '\n\n'; } // Close message rawMessage += `--${boundary}--`; const command = new SendRawEmailCommand({ RawMessage: { Data: Buffer.from(rawMessage) } }); const result = await ses.send(command); return { messageId: result.MessageId }; } async function downloadFileFromUrl(url: string): Promise { try { const response = await axios.get(url, { responseType: 'arraybuffer' }); return Buffer.from(response.data); } catch (error) { logger.error(`Error downloading file from URL: ${error}`); throw new Error(`Failed to download file: ${error}`); } } export const sendEmailSES = onRequest({ region: '#{SERVICES_RGN}#' }, (request: Request, response: Response) => { return corsHandler(request, response, async () => { try { const toAddress = request.body.toAddress; const subject = request.body.subject; const message = request.body.message; // Initialize data with basic fields const data: EmailRequest = { to: toAddress, html: message, subject: subject, text: stripHtml(message), from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', attachments: request.body.attachments as Attachment[] || [] }; // Handle file URL if provided if (request.body.fileUrl && request.body.fileName) { logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); try { const fileContent = await downloadFileFromUrl(request.body.fileUrl); // If attachments array doesn't exist, create it if (!data.attachments) { data.attachments = []; } // Add the downloaded file as an attachment data.attachments.push({ filename: request.body.fileName, content: fileContent, contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' }); logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); } catch (downloadError) { logger.error(`Failed to download attachment: ${downloadError}`); throw new Error(`Failed to process attachment: ${downloadError}`); } } if (!data.to || !data.subject || !data.html || !data.from) { throw new HttpsError( 'invalid-argument', 'Missing required email fields' ); } logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); const recipients = Array.isArray(data.to) ? data.to : [data.to]; if (data.attachments && data.attachments.length > 0) { const messageResult = await sendEmailWithAttachments(data, recipients); response.status(200).json(messageResult); } else { const messageResult = await sendSimpleEmail(data, recipients); response.status(200).json(messageResult); } } catch (e) { logger.error(`Error while sending E-mail. Error: ${e}`); console.error(`Error while sending E-mail. Error: ${e}`); response.status(500).json({ success: false, error: 'Error while sending E-mail' }); } }); });