import nodemailer from 'nodemailer'; import dotenv from 'dotenv'; import {Router} from 'express'; /** @typedef Email * @property {string} name - name of sender. * @property {string} email - email addr of sender. * @property {string} subject - subject of email. * @property {string} message - message body of email. */ /** @typedef {import("@types/express").Request} Request */ /** @typedef {import("@types/express").Response} Response */ /** @typedef {import("@types/express").NextFunction} NextFunction */ /** @typedef {import("@types/nodemailer").TransportOptions} TransportOptions */ dotenv.config(); const transporter = nodemailer.createTransport( /** @type {TransportOptions}*/ { service: 'gmail', // Use Gmail as the email service target: process.env.EMAIL_TARGET, // The email address to send the email to auth: { user: process.env.MAILER_EMAIL_ADDR, // Your Gmail address pass: process.env.MAILER_EMAIL_PASS, // Your Gmail password or App Password }, }); await withTimeout(transporter.verify(), 15000) .then(() => { console.log("Transporter is ready to send emails"); }) .catch((error) => console.error("Error verifying transporter:", error)); const router = Router() router.post('/', /** * Sends an email using the configured transporter. * @param {Request} req * @param {Response} res * @param {NextFunction} next * @return {Promise> | void>} */ async (req, res, next) => { try { console.debug("made it to sendEmail function"); let exitonError = false; await withTimeout(transporter.verify(), 15000) .then(() => { console.debug("Transporter is ready to send emails"); }) .catch((error) => { console.error("Error verifying transporter:", error); res.status(500).json({message: 'Failed to verify email transporter'}); exitonError = true; }); if (exitonError) return; const emailWithZeroWidthSpace = req.body.email.replace(/@/g, '@\u200B').replace(/\./g, ".\u200B"); // Add zero-width space to prevent email scraping /** @type {Email} */ const contact = { name: req.body.name, email: emailWithZeroWidthSpace, // Add zero-width space to prevent email scraping subject: req.body.subject, message: req.body.message, }; if (!contact.name || !contact.email || !contact.subject || !contact.message) { return res.status(400).json({message: 'To, subject, and text are required'}); } const emailText = ` This email was sent from Li0nhunter's Bot. Name: ${contact.name} Email: ${contact.email} Subject: ${contact.subject} Message: ${contact.message} Respond now: mailto:${contact.email} ` const emailHtml = `

You got a message from your website's contact page!

${contact.name}
${contact.email}
${contact.subject}
${contact.message}
Respond now

This email was sent from Li0nhunter's Bot.

`; const mailOptions = { from: `"Li0nhunter's Web Bot" <${process.env.EMAIL_ADDR}>`, // sender address to: process.env.EMAIL_TARGET, replyTo: contact.email, subject: contact.subject, // Subject line text: emailText, // plain text body html: emailHtml, // html body }; // Set a timeout of 15 seconds (15,000 ms) for sending the email await withTimeout(transporter.sendMail(mailOptions), 15000) .then((result) => res.status(200).json({data: result, message: 'Email sent successfully'})) .catch((error) => { console.error(error); res.status(500).json({message: error.message || 'Failed to send email'}); }); } catch (error) { next(error); } }); /** * Executes a promise with a timeout. * @param {Promise} promise The promise to execute. * @param {number} ms The timeout in milliseconds. * @throws {Error} If the promise does not resolve within the specified time. * @return {Promise} */ async function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Operation timed out')), ms) ); return Promise.race([promise, timeout]) } export default router;