From d9cd772f6e41d910de3e6935d7b96d3f9d376a0b Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 27 May 2025 18:26:13 +0000 Subject: [PATCH 1/5] 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; } } From 7482b20526db35b74862f89e56d9ef823627b0a6 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 27 May 2025 18:46:11 +0000 Subject: [PATCH 2/5] phonepe (#52) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/52 --- functions/src/payments/phonepe/webhook.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 2ba5136..8f797d7 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -172,6 +172,7 @@ export const phonePeWebhook = onRequest({ let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership'; let trainerId = orderData.metaInfo?.trainerId; let trainerData = null; + let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address']; const discountPercentage = orderData.metaInfo?.discount || 0; const hasDiscount = discountPercentage > 0; @@ -308,7 +309,7 @@ export const phonePeWebhook = onRequest({ const formattedDate = format(new Date(), 'dd/MM/yyyy'); - if (membershipData?.fields?.['email']) { + if (emailCustomer) { logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`); try { const emailSubject = isFreeplan From ee1619ae3817a59a17710846833d9eea38234663 Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 27 May 2025 19:00:58 +0000 Subject: [PATCH 3/5] phonepe (#53) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/53 --- functions/src/payments/phonepe/webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index 8f797d7..f9c3591 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -343,7 +343,7 @@ export const phonePeWebhook = onRequest({ `; await sendEmailWithAttachmentUtil( - membershipData?.fields?.['email'], + emailCustomer, emailSubject, customerEmailHtml, downloadUrl, From 3f4b30743179969800512c7ab30bb7e65e48c88f Mon Sep 17 00:00:00 2001 From: Allen T J Date: Tue, 27 May 2025 19:14:40 +0000 Subject: [PATCH 4/5] phonepe (#54) Co-authored-by: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/54 --- functions/src/payments/phonepe/webhook.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/src/payments/phonepe/webhook.ts b/functions/src/payments/phonepe/webhook.ts index f9c3591..bf09946 100644 --- a/functions/src/payments/phonepe/webhook.ts +++ b/functions/src/payments/phonepe/webhook.ts @@ -243,9 +243,9 @@ export const phonePeWebhook = onRequest({ businessName: gymName, address: gymAddress, gstNumber: userData?.gstNumber, - customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(), - phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '', - email: membershipData?.fields?.['email'] || '', + customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '', + phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '', + email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '', planName: orderData.metaInfo?.planName || subscriptionName, amount: orderData.amount, transactionId: payload.orderId, From c43c9cf26c8b4b62a35b8fec96478fca77143540 Mon Sep 17 00:00:00 2001 From: Benoy Bose Date: Wed, 28 May 2025 10:55:08 +0530 Subject: [PATCH 5/5] Removed dependency of mailgun --- functions/.env.example | 3 --- functions/package-lock.json | 24 ------------------------ functions/package.json | 1 - 3 files changed, 28 deletions(-) diff --git a/functions/.env.example b/functions/.env.example index e62bb23..c19a79d 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -1,6 +1,3 @@ -MAILGUN_API_KEY=#{MAILGUN_API_KEY}# -MAILGUN_SERVER=#{MAILGUN_SERVER}# -MAILGUN_FROM_ADDRESS=#{MAILGUN_FROM_ADDRESS}# TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}# TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}# TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}# diff --git a/functions/package-lock.json b/functions/package-lock.json index 149bfc8..853a3bf 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -22,7 +22,6 @@ "jspdf": "^3.0.1", "jspdf-autotable": "^5.0.2", "long": "^5.3.2", - "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", "pdfmake": "^0.2.20", @@ -3277,11 +3276,6 @@ "dev": true, "peer": true }, - "node_modules/base-64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" - }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -6405,19 +6399,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/mailgun.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-10.4.0.tgz", - "integrity": "sha512-YrdaZEAJwwjXGBTfZTNQ1LM7tmkdUaz2NpZEu7+zULcG4Wrlhd7cWSNZW0bxT3bP48k5N0mZWz8C2f9gc2+Geg==", - "dependencies": { - "axios": "^1.7.4", - "base-64": "^1.0.0", - "url-join": "^4.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8111,11 +8092,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/functions/package.json b/functions/package.json index 0e77a8f..b29002f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -29,7 +29,6 @@ "jspdf": "^3.0.1", "jspdf-autotable": "^5.0.2", "long": "^5.3.2", - "mailgun.js": "^10.4.0", "node-fetch": "^2.7.0", "pdfjs-dist": "^5.0.375", "pdfmake": "^0.2.20",