I have a react app and Vercel api connected with Stripe. I have set up a webhook where after a successful payment the email should be sent to my gmail with all the information about the order as well as images of the items. It all works very well if I attach up to 2 images. If there are more than 2 images attached the email does not get sent and no error messages are returned.
The webhook code
import { buffer } from "micro";
import Stripe from "stripe";
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { storage } from "../src/utils/firebase.js";
import { ref, getDownloadURL } from "firebase/storage";
import bucket from "../src/utils/firebaseAdmin.js";
export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method === "POST") {
let event: { [key: string]: any };
const stripe = new Stripe( "sk_test...........",
{ apiVersion: "2022-11-15" }
);
try {
const requestBuffer = await buffer(req);
const signature = req.headers["stripe-signature"] as string;
event = stripe.webhooks.constructEvent(
requestBuffer.toString(),
signature,
"whsec_................"
);
} catch (error) {
res.status(400).send(`Webhook error: ${error}`);
return;
}
//remove images from firebase
const deleteImageFromFirebase = async (user: string) => {
await bucket.deleteFiles({ prefix: `temp/${user}` });
};
const user = event.data.object.metadata.user;
switch (event.type) {
case "checkout.session.completed": {
const sessionWithLineItems = await stripe.checkout.sessions.retrieve(
event.data.object.id,
{
expand: ["line_items"],
}
);
const lineItems = sessionWithLineItems.line_items;
const purchasedItems = lineItems?.data.map(
(item: { [key: string]: any }) => {
return item.description;
}
);
//send email to own email about new order
let imgUrls: { [key: string]: any }[] = [];
for (let i = 0; (purchasedItems as string[]).length > i; i++) {
const myRef = ref(storage, `temp/${user}/${i}.png`);
const downloadUrl = await getDownloadURL(myRef);
imgUrls.push({ filename: `${i + 1}.png`, path: downloadUrl });
}
const email = event.data.object.customer_details.email;
const name = event.data.object.customer_details.name;
const to = "[email protected]";
const from = `New order from ${name} <.....@....>`;
const subject = `There is a new order from ${name}`;
try {
await fetch("http://localhost:3001/api/submitForm", {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
name: `${name}`,
email: `${email}`,
message: `There is a new order:
${purchasedItems?.map((item: string, i: number) => {
return ` ${i + 1}: ${item}`;
})}
`,
attachments: imgUrls,
to: to,
from: from,
subject: subject,
date: date,
toCustomer: false,
}),
}).then(() => {
//remove temporary images from firebase
deleteImageFromFirebase(user);
});
} catch (e) {
console.log(e)
res.status(500).send(`Webhook error: ${e}`);
}
res.status(200);
break;
}
case "checkout.session.expired": {
//remove temporary images from database
deleteImageFromFirebase(user);
break;
}
}
}
}
Nodemailer code
import type { VercelRequest, VercelResponse } from '@vercel/node';
import nodemailer from "nodemailer";
const allowCors =
(fn: any) => async (req: VercelRequest, res: VercelResponse) => {
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Methods",
"GET,OPTIONS,PATCH,DELETE,POST,PUT"
);
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
);
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
return await fn(req, res);
};
async function submitForm(req: VercelRequest, res: VercelResponse) {
if (req.method === "POST") {
const name = req.body.name;
const email = req.body.email;
const message = req.body.message;
const to = req.body.to;
const from = req.body.from;
const subject = req.body.subject;
const attachments = req.body.attachments || [];
const toCustomer = req.body.toCustomer;
const contactEmail = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
await new Promise((resolve, reject) => {
contactEmail.verify((error, success) => {
if (error) {
console.log(error);
reject(error);
res.status(500).send(`Nodemailer error: ${error}`);
} else {
console.log("Ready to Send");
resolve(success);
}
});
});
const mailInfo = {
from: from,
to: to,
subject: subject,
html: toCustomer
? `<p>${message}</p>`
: `<p>Nombre: ${name}</p>
<p>Correo: ${email}</p>
<p>Mensaje: ${message}</p>`,
attachments: attachments,
};
await new Promise((resolve, reject) => {
console.log("inside promise before send")
contactEmail.sendMail(mailInfo, (error, info) => {
console.log("inside send");
if (error) {
console.log(error)
reject(error);
res.status(500).send(`Nodemailer error: ${error}`);
} else {
console.log(info)
resolve(info);
res.status(200);
}
});
});
console.log("end")
}
}
export default allowCors(submitForm);
logs when I am attaching 1 image (850kb - 2.5mb)
Ready to Send
inside promise before send
inside send
{
...info
}
end
logs when I am attaching 4 images or more (~800b/img)
Ready to Send
inside promise before send
logs of attachments
[ { filename: '1.png', path: 'firebasestorage.googleapis.com...' }, { filename: '2.png', path: 'firebasestorage.googleapis.com...' }, { filename: '3.png', path: 'firebasestorage.googleapis.com...' }, { filename: '4.png', path: 'firebasestorage.googleapis.com...' } ]
It never reaches "inside send" I was looking through everywhere and tried changing the code without any luck. Any ideas of what can be wrong or could fix it would be appreciated.
mailInfoand see if there is a threshold?attachmentsvariable contains? Feel free to anonymize any PII in the filenames but it might help understanding if the value matches the expected format here: nodemailer.com/message/attachments