diff --git a/backend/src/controllers/emailController.js b/backend/src/controllers/emailController.js index 3aa8906..c814e84 100644 --- a/backend/src/controllers/emailController.js +++ b/backend/src/controllers/emailController.js @@ -1,28 +1,55 @@ const nodemailer = require('nodemailer'); -const { sendEmail, transporter } = require('../utils/sendEmail'); +const { sendEmail } = require('../utils/SendEmail'); const logger = require('../utils/logger'); +const mongoose = require('mongoose'); +const EmailEvent = require('../models/EmailEvent'); -//POST /api/emails/send -//Body: {to,subject,text,html,from} +// POST /api/emails/send +// Body: { to, subject, text, html, from } exports.send = async (req, res) => { try { const { to, subject, text, html, from } = req.body; - if (!to) return res.status(400).json({ success: false, error: 'Recipient "to" is required' }); + if (!to) { + return res.status(400).json({ success: false, error: 'Recipient "to" is required' }); + } + + // create unique emailId for tracking + const emailId = new mongoose.Types.ObjectId(); + + // ensure base URL is defined + const baseUrl = process.env.BASE_URL || 'http://localhost:5000'; + + // inject tracking pixel and link into HTML body + const trackedHtml = ` + ${html || ''} + +
+ `; + + // send email through configured transporter + const info = await sendEmail({ to, subject, text, html: trackedHtml, from }); - const info = await sendEmail({ to, subject, text, html, from }); + // store initial record in database + await EmailEvent.create({ + _id: emailId, + recipient: to, + subject, + status: 'sent', + createdAt: new Date() + }); return res.status(200).json({ success: true, - message: 'Email sent', + message: 'Email sent (with tracking)', info: { messageId: info.messageId, accepted: info.accepted, rejected: info.rejected, response: info.response - } + }, + emailId }); } catch (error) { - //full error.message for debugging logger.error('Error in send email controller:', error && (error.stack || error)); return res.status(500).json({ success: false, @@ -31,33 +58,49 @@ exports.send = async (req, res) => { } }; -//POST /api/emails/test -//Body optional: {to} provide in body or in .env +// POST /api/emails/test exports.test = async (req, res) => { try { - const to = req.body?.to || process.env.EMAIL_TEST_TO || process.env.SMTP_USER || process.env.EMAIL_USER; + const to = + req.body?.to || + process.env.EMAIL_TEST_TO || + process.env.SMTP_USER || + process.env.EMAIL_USER; if (!to) { return res.status(400).json({ success: false, - error: 'No test recipient configured. Provide "to" in body or set EMAIL_TEST_TO/SMTP_USER/EMAIL_USER in env.' + error: + 'No test recipient configured. Provide "to" in body or set EMAIL_TEST_TO/SMTP_USER/EMAIL_USER in env.' }); } - const subject = 'MailMERN — Test Email'; - const text = `MailMERN test email sent at ${new Date().toISOString()}`; - const html = `MailMERN test email sent at ${new Date().toISOString()}
`; + const subject = 'MailMERN — Test Email (Tracked)'; + const html = `Hello! This is a MailMERN tracking test email.
`; + + const emailId = new mongoose.Types.ObjectId(); + const baseUrl = process.env.BASE_URL || 'http://localhost:5000'; + + const trackedHtml = ` + ${html} + + + `; - const info = await sendEmail({ to, subject, text, html }); + const info = await sendEmail({ to, subject, html: trackedHtml }); + + await EmailEvent.create({ + _id: emailId, + recipient: to, + subject, + status: 'sent', + createdAt: new Date() + }); return res.status(200).json({ success: true, - message: `Test email sent to ${to}`, - info: { - messageId: info.messageId, - accepted: info.accepted, - rejected: info.rejected, - response: info.response - } + message: `Test tracked email sent to ${to}`, + info, + emailId }); } catch (error) { logger.error('Error in email test controller:', error && (error.stack || error)); @@ -68,9 +111,7 @@ exports.test = async (req, res) => { } }; - - //POST /api/emails/test-ethereal - //Creates a temporary Ethereal account and sends a test email there.useful to check sending code path without real SMTP credentials +// POST /api/emails/test-ethereal exports.testEthereal = async (req, res) => { try { const testAccount = await nodemailer.createTestAccount(); @@ -87,14 +128,12 @@ exports.testEthereal = async (req, res) => { const to = req.body?.to || process.env.EMAIL_TEST_TO || 'recipient@example.com'; const subject = 'MailMERN — Ethereal Test Email'; - const text = `Ethereal test email sent at ${new Date().toISOString()}`; const html = `Ethereal test email sent at ${new Date().toISOString()}
`; const info = await ethTransport.sendMail({ from: process.env.EMAIL_FROM || `MailMERN <${testAccount.user}>`, to, subject, - text, html }); @@ -122,4 +161,4 @@ exports.testEthereal = async (req, res) => { error: error && (error.message || error) }); } -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/trackController.js b/backend/src/controllers/trackController.js new file mode 100644 index 0000000..71622ee --- /dev/null +++ b/backend/src/controllers/trackController.js @@ -0,0 +1,52 @@ +// controllers/trackingController.js +const path = require('path'); +const fs = require('fs'); +const EmailEvent = require('../models/EmailEvent'); +const logger = require('../utils/logger'); + +exports.trackOpen = async (req, res) => { + const { emailId } = req.params; + try { + await EmailEvent.create({ + email: emailId, // ✅ required field + eventType: 'opened', + timestamp: new Date() + }); + logger.info(`Open event logged for ${emailId}`); + } catch (err) { + logger.error('Error logging open event:', err); + } + + const pixelPath = path.join(__dirname, '../assets/pixel.png'); + if (fs.existsSync(pixelPath)) { + res.setHeader('Content-Type', 'image/png'); + fs.createReadStream(pixelPath).pipe(res); + } else { + const img = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFxgJ/2fT8ZgAAAABJRU5ErkJggg==', + 'base64' + ); + res.setHeader('Content-Type', 'image/png'); + res.end(img); + } +}; + +exports.trackClick = async (req, res) => { + const { emailId } = req.params; + const redirect = req.query.redirect; + + try { + await EmailEvent.create({ + email: emailId, // ✅ required field + eventType: 'clicked', + timestamp: new Date(), + metadata: { redirect } + }); + logger.info(`Click event logged for ${emailId}`); + } catch (err) { + logger.error('Error logging click event:', err); + } + + if (redirect) return res.redirect(redirect); + res.status(400).send('Missing redirect URL'); +}; diff --git a/backend/src/middlewares/errorMiddleware.js b/backend/src/middlewares/errorMiddleware.js index c76ad33..4b2ca55 100644 --- a/backend/src/middlewares/errorMiddleware.js +++ b/backend/src/middlewares/errorMiddleware.js @@ -1,10 +1,10 @@ -export function errorMiddleware(err, req, res, next) { +export function errorMiddleware(err, req, res, next) { const statusCode = err.statusCode || 500; const message = err.message || 'Internal Server Error'; - res.status(statusCode).json({ + res.status(statusCode).json({ success: false, message, stack: process.env.NODE_ENV === 'production' ? null : err.stack - }); - next(); + }); + next(); } \ No newline at end of file diff --git a/backend/src/models/EmailEvent.js b/backend/src/models/EmailEvent.js new file mode 100644 index 0000000..ed7a5d3 --- /dev/null +++ b/backend/src/models/EmailEvent.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); + +const emailEventSchema = new mongoose.Schema({ + email: { type: String, required: true }, + subject: { type: String }, + messageId: { type: String }, + status: { type: String, enum: ['sent', 'opened', 'clicked'], default: 'sent' }, + eventType: { type: String }, + createdAt: { type: Date, default: Date.now } +}); + +module.exports = mongoose.model('EmailEvent', emailEventSchema); diff --git a/backend/src/routes/trackRoutes.js b/backend/src/routes/trackRoutes.js new file mode 100644 index 0000000..f02a112 --- /dev/null +++ b/backend/src/routes/trackRoutes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const { trackOpen, trackClick } = require('../controllers/trackController'); +router.get('/open/:emailId.png', trackOpen); +router.get('/click/:emailId', trackClick); + +module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js index 68641ed..7bb18d2 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,5 +1,4 @@ require('dotenv').config(); - const express = require('express'); const cors = require('cors'); const connectDB = require('./config/db'); @@ -7,6 +6,8 @@ const userRoutes = require('./routes/userRoutes'); const { errorMiddleware } = require('./middlewares/errorMiddleware'); const chatbotRoutes = require('./routes/chatbotRoutes'); const emailRoutes = require('./routes/emailRoutes'); +const trackRoutes = require('./routes/trackRoutes'); +const { configDotenv } = require('dotenv'); const contactRoutes = require('./routes/contactRoutes'); const app = express(); app.use( @@ -22,6 +23,7 @@ app.use('/api/users', userRoutes); app.use('/api/chatbot', chatbotRoutes); app.use('/api/auth', userRoutes); app.use('/api/emails', emailRoutes); +app.use('/api/track', trackRoutes); app.use('/api/contacts',contactRoutes); const PORT = process.env.PORT || 5000; diff --git a/package.json b/package.json index ce4ac90..3c468c7 100644 --- a/package.json +++ b/package.json @@ -13,4 +13,4 @@ "nodemon": "^3.1.10", "react-hot-toast": "^2.6.0" } -} +} \ No newline at end of file