From d9cd772f6e41d910de3e6935d7b96d3f9d376a0b Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 27 May 2025 18:26:13 +0000 Subject: [PATCH] phonepe (#51) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/51 --- functions/src/utils/emailService.ts | 231 +++++++++++++++++++++------- 1 file changed, 178 insertions(+), 53 deletions(-) diff --git a/functions/src/utils/emailService.ts b/functions/src/utils/emailService.ts index b7f70a8..1bb4e06 100644 --- a/functions/src/utils/emailService.ts +++ b/functions/src/utils/emailService.ts @@ -1,15 +1,135 @@ -import * as os from 'os'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as https from 'https'; -import { getLogger } from "../shared/config"; -import formData from 'form-data'; -import Mailgun from 'mailgun.js'; -const { convert } = require('html-to-text'); -const mailgun = new Mailgun(formData); +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; // 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 async function sendEmailWithAttachmentUtil( toAddress: string, subject: string, @@ -18,53 +138,58 @@ export async function sendEmailWithAttachmentUtil( fileName?: string ): Promise { try { - const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf'); - await new Promise((resolve, reject) => { - const file = fs.createWriteStream(tempFilePath); - https.get(fileUrl, (res) => { - res.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); + logger.info(`Sending email with attachment to: ${toAddress}`); + + // 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: [] + }; + + // Handle file URL if provided + if (fileUrl && fileName) { + logger.info(`Downloading attachment from URL: ${fileUrl}`); + try { + const fileContent = await downloadFileFromUrl(fileUrl); + + // Add the downloaded file as an attachment + data.attachments!.push({ + filename: fileName, + content: fileContent, + contentType: mime.lookup(fileName) || 'application/octet-stream' }); - }).on('error', (err) => { - fs.unlink(tempFilePath, () => {}); - reject(err); - }); - }); - - try { - const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! }); - const options = { - wordwrap: 130, - }; - const textMessage = convert(message, options); - const fileBuffer = fs.readFileSync(tempFilePath); - const attachmentFilename = fileName || path.basename(fileUrl.split('?')[0]); - - const data = { - from: process.env.MAILGUN_FROM_ADDRESS, - to: toAddress, - subject: subject, - text: textMessage, - html: message, - attachment: { - data: fileBuffer, - filename: attachmentFilename, - contentType: 'application/pdf', - } - }; - - const result = await client.messages.create(process.env.MAILGUN_SERVER!, data); - fs.unlinkSync(tempFilePath); - logger.info('Email with attachment from URL sent successfully'); - return { success: true, result }; - } catch (e) { - logger.error(`Error while sending E-mail. Error: ${e}`); - throw e; + + 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 from URL:', error); + logger.error('Error sending email with attachment via SES:', error); throw error; } }