Compare commits
No commits in common. "7a796243b0a4fa53d53311bf06503819fb284180" and "7db9e479ad660bf2c8ac88e2aa169daa6fcc5e1d" have entirely different histories.
7a796243b0
...
7db9e479ad
@ -1,3 +1,6 @@
|
|||||||
|
MAILGUN_API_KEY=#{MAILGUN_API_KEY}#
|
||||||
|
MAILGUN_SERVER=#{MAILGUN_SERVER}#
|
||||||
|
MAILGUN_FROM_ADDRESS=#{MAILGUN_FROM_ADDRESS}#
|
||||||
TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}#
|
TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}#
|
||||||
TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}#
|
TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}#
|
||||||
TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}#
|
TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}#
|
||||||
|
|||||||
24
functions/package-lock.json
generated
24
functions/package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"long": "^5.3.2",
|
"long": "^5.3.2",
|
||||||
|
"mailgun.js": "^10.4.0",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"pdfjs-dist": "^5.0.375",
|
"pdfjs-dist": "^5.0.375",
|
||||||
"pdfmake": "^0.2.20",
|
"pdfmake": "^0.2.20",
|
||||||
@ -3276,6 +3277,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"node_modules/base64-arraybuffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
@ -6399,6 +6405,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"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": {
|
"node_modules/make-dir": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||||
@ -8092,6 +8111,11 @@
|
|||||||
"querystring": "0.2.0"
|
"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": {
|
"node_modules/util": {
|
||||||
"version": "0.12.5",
|
"version": "0.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"long": "^5.3.2",
|
"long": "^5.3.2",
|
||||||
|
"mailgun.js": "^10.4.0",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"pdfjs-dist": "^5.0.375",
|
"pdfjs-dist": "^5.0.375",
|
||||||
"pdfmake": "^0.2.20",
|
"pdfmake": "^0.2.20",
|
||||||
|
|||||||
@ -172,7 +172,6 @@ export const phonePeWebhook = onRequest({
|
|||||||
let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership';
|
let paymentType = orderData.metaInfo?.paymentType || 'Gym Membership';
|
||||||
let trainerId = orderData.metaInfo?.trainerId;
|
let trainerId = orderData.metaInfo?.trainerId;
|
||||||
let trainerData = null;
|
let trainerData = null;
|
||||||
let emailCustomer = membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'];
|
|
||||||
|
|
||||||
const discountPercentage = orderData.metaInfo?.discount || 0;
|
const discountPercentage = orderData.metaInfo?.discount || 0;
|
||||||
const hasDiscount = discountPercentage > 0;
|
const hasDiscount = discountPercentage > 0;
|
||||||
@ -243,9 +242,9 @@ export const phonePeWebhook = onRequest({
|
|||||||
businessName: gymName,
|
businessName: gymName,
|
||||||
address: gymAddress,
|
address: gymAddress,
|
||||||
gstNumber: userData?.gstNumber,
|
gstNumber: userData?.gstNumber,
|
||||||
customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim() || membershipData?.fields?.['First Name'] || '',
|
customerName: userData?.displayName || `${membershipData?.fields?.['first-name'] || ''} ${membershipData?.fields?.['last-name'] || ''}`.trim(),
|
||||||
phoneNumber: membershipData?.fields?.['phone-number'] || membershipData?.fields?.['Phone Number'] || orderData.metaInfo?.phoneNumber || '',
|
phoneNumber: membershipData?.fields?.['phone-number'] || orderData.metaInfo?.phoneNumber || '',
|
||||||
email: membershipData?.fields?.['email'] || membershipData?.fields?.['Email Address'] || '',
|
email: membershipData?.fields?.['email'] || '',
|
||||||
planName: orderData.metaInfo?.planName || subscriptionName,
|
planName: orderData.metaInfo?.planName || subscriptionName,
|
||||||
amount: orderData.amount,
|
amount: orderData.amount,
|
||||||
transactionId: payload.orderId,
|
transactionId: payload.orderId,
|
||||||
@ -309,7 +308,7 @@ export const phonePeWebhook = onRequest({
|
|||||||
|
|
||||||
const formattedDate = format(new Date(), 'dd/MM/yyyy');
|
const formattedDate = format(new Date(), 'dd/MM/yyyy');
|
||||||
|
|
||||||
if (emailCustomer) {
|
if (membershipData?.fields?.['email']) {
|
||||||
logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`);
|
logger.info(`Preparing to send invoice email to customer: ${membershipData?.fields?.['email']}`);
|
||||||
try {
|
try {
|
||||||
const emailSubject = isFreeplan
|
const emailSubject = isFreeplan
|
||||||
@ -343,7 +342,7 @@ export const phonePeWebhook = onRequest({
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
await sendEmailWithAttachmentUtil(
|
await sendEmailWithAttachmentUtil(
|
||||||
emailCustomer,
|
membershipData?.fields?.['email'],
|
||||||
emailSubject,
|
emailSubject,
|
||||||
customerEmailHtml,
|
customerEmailHtml,
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
|
|||||||
@ -1,135 +1,15 @@
|
|||||||
|
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 { getLogger } from "../shared/config";
|
||||||
import { SESClient } from "@aws-sdk/client-ses";
|
import formData from 'form-data';
|
||||||
import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses";
|
import Mailgun from 'mailgun.js';
|
||||||
import * as mime from 'mime-types';
|
const { convert } = require('html-to-text');
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
|
const mailgun = new Mailgun(formData);
|
||||||
const logger = getLogger();
|
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<Buffer> {
|
|
||||||
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(
|
export async function sendEmailWithAttachmentUtil(
|
||||||
toAddress: string,
|
toAddress: string,
|
||||||
subject: string,
|
subject: string,
|
||||||
@ -138,58 +18,53 @@ export async function sendEmailWithAttachmentUtil(
|
|||||||
fileName?: string
|
fileName?: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Sending email with attachment to: ${toAddress}`);
|
const tempFilePath = path.join(os.tmpdir(), fileName || 'attachment.pdf');
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
// Initialize data with basic fields
|
const file = fs.createWriteStream(tempFilePath);
|
||||||
const data: EmailRequest = {
|
https.get(fileUrl, (res) => {
|
||||||
to: toAddress,
|
res.pipe(file);
|
||||||
html: message,
|
file.on('finish', () => {
|
||||||
subject: subject,
|
file.close();
|
||||||
text: stripHtml(message),
|
resolve();
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
logger.info(`Successfully downloaded attachment: ${fileName}`);
|
try {
|
||||||
} catch (downloadError) {
|
const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY! });
|
||||||
logger.error(`Failed to download attachment: ${downloadError}`);
|
const options = {
|
||||||
throw new Error(`Failed to process attachment: ${downloadError}`);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
logger.error('Error sending email with attachment via SES:', error);
|
logger.error('Error sending email with attachment from URL:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user