import { getLogger } from "../shared/config"; import { SESClient } from "@aws-sdk/client-ses"; import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; import * as mime from 'mime-types'; import axios from 'axios'; const logger = getLogger(); 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; 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: process.env.AWS_REGION, 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: process.env.AWS_REGION, 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`; rawMessage += `--${boundary}\n`; rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; if (data.text) { rawMessage += `--alt_${boundary}\n`; rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; rawMessage += `${data.text}\n\n`; } rawMessage += `--alt_${boundary}\n`; rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; rawMessage += `${data.html}\n\n`; rawMessage += `--alt_${boundary}--\n\n`; 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'; } 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 async function sendEmailWithAttachmentUtil( toAddress: string, subject: string, message: string, fileUrl: string, fileName?: string ): Promise { try { logger.info(`Sending email with attachment to: ${toAddress}`); 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: [] }; if (fileUrl && fileName) { logger.info(`Downloading attachment from URL: ${fileUrl}`); try { const fileContent = await downloadFileFromUrl(fileUrl); data.attachments!.push({ filename: fileName, content: fileContent, contentType: mime.lookup(fileName) || 'application/octet-stream' }); logger.info(`Successfully downloaded attachment: ${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 Error('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]; let result; if (data.attachments && data.attachments.length > 0) { result = await sendEmailWithAttachments(data, recipients); } else { result = await sendSimpleEmail(data, recipients); } logger.info('Email sent successfully via SES'); return { success: true, result }; } catch (error) { logger.error('Error sending email with attachment via SES:', error); throw error; } }