Skip to content

Commit 89b2bc9

Browse files
Enhance email service by adding HTML email wrapping and preheader support; implement cron job for daily connection request summaries
1 parent 772265b commit 89b2bc9

4 files changed

Lines changed: 209 additions & 52 deletions

File tree

src/routes/auth.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const crypto = require("crypto");
88
const axios = require("axios");
99
const sendEmail = require("../services/emailService");
1010
const UAparser = require("ua-parser-js");
11+
const { escapeHtml } = require("../services/emailTemplates");
1112

1213
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
1314

@@ -42,11 +43,13 @@ authRouter.post("/signup", async (req, res) => {
4243
await sendEmail({
4344
to: savedUser.email,
4445
subject: "Verify your DevSync account",
46+
preheader: "Confirm your email address to finish setting up your account.",
4547
html:
4648
`
47-
<h2>Welcome to DevSync, ${savedUser.firstName}</h2>
48-
<p>Please verify your email.</p>
49-
<a href="${verifyLink}">Verify Email</a>
49+
<h2 style="margin:0 0 8px;">Welcome to DevSync, ${escapeHtml(savedUser.firstName)}</h2>
50+
<p style="margin:0 0 12px;">Please confirm your email address to activate your account.</p>
51+
<p style="margin:0 0 12px;"><a href="${verifyLink}">${verifyLink}</a></p>
52+
<p style="margin:0;">If you didn’t create this account, you can ignore this email.</p>
5053
`
5154
})
5255

@@ -126,11 +129,13 @@ authRouter.post("/resend-verification", async (req, res) => {
126129
await sendEmail({
127130
to: user.email,
128131
subject: "Verify your DevSync account",
132+
preheader: "Your new verification link is inside.",
129133
html:
130134
`
131-
<h2>Welcome to DevSync, ${user.firstName}</h2>
132-
<p>Please verify your email.</p>
133-
<a href="${verifyLink}">Verify Email</a>
135+
<h2 style="margin:0 0 8px;">Hi ${escapeHtml(user.firstName)},</h2>
136+
<p style="margin:0 0 12px;">Here’s your new email verification link:</p>
137+
<p style="margin:0 0 12px;"><a href="${verifyLink}">${verifyLink}</a></p>
138+
<p style="margin:0;">If you didn’t request this, you can ignore this email.</p>
134139
`
135140
})
136141

@@ -191,16 +196,16 @@ authRouter.post("/login", async (req, res) => {
191196
await sendEmail({
192197
to: user.email,
193198
subject: "New login detected on DevSync",
199+
preheader: "We noticed a login from a new device.",
194200
html: `
195-
<h3>New Login Detected</h3>
196-
197-
<p>A new login to your DevSync account was detected.</p>
198-
199-
<b>Device:</b> ${deviceName} <br/>
200-
<b>IP:</b> ${ip} <br/>
201-
<b>Time:</b> ${new Date().toLocaleString()} <br/>
202-
203-
<p>If this wasn't you, please reset your password.</p>
201+
<h3 style="margin:0 0 8px;">New login detected</h3>
202+
<p style="margin:0 0 12px;">We noticed a login to your DevSync account from a new device.</p>
203+
<div style="margin:0 0 12px;">
204+
<div><b>Device:</b> ${escapeHtml(deviceName)}</div>
205+
<div><b>IP:</b> ${escapeHtml(ip)}</div>
206+
<div><b>Time:</b> ${escapeHtml(new Date().toLocaleString())}</div>
207+
</div>
208+
<p style="margin:0;">If this wasn’t you, please reset your password immediately.</p>
204209
`
205210
});
206211

@@ -260,7 +265,13 @@ authRouter.post("/forgot-password", async (req, res) => {
260265
await sendEmail({
261266
to: user.email,
262267
subject: "Reset your DevSync password",
263-
html: `<a href="${resetLink}">Reset Password</a>`
268+
preheader: "Use this link to reset your password (valid for 1 hour).",
269+
html: `
270+
<p style="margin:0 0 12px;">We received a request to reset your DevSync password.</p>
271+
<p style="margin:0 0 12px;">Reset your password using this link (valid for 1 hour):</p>
272+
<p style="margin:0 0 12px;"><a href="${resetLink}">${resetLink}</a></p>
273+
<p style="margin:0;">If you didn’t request this, you can safely ignore this email.</p>
274+
`
264275
});
265276

266277
res.json({ message: "If the account exists, a reset email has been sent" });

src/services/emailService.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const nodemailer = require('nodemailer');
2+
const { wrapEmail } = require("./emailTemplates");
23

34

45
const transporter = nodemailer.createTransport({
@@ -11,12 +12,12 @@ const transporter = nodemailer.createTransport({
1112
}
1213
});
1314

14-
const sendEmail = async ({ to, subject, html }) => {
15+
const sendEmail = async ({ to, subject, html, preheader }) => {
1516
await transporter.sendMail({
1617
from: `DevSync <${process.env.EMAIL_FROM}>`,
1718
to,
1819
subject,
19-
html
20+
html: wrapEmail({ subject, preheader, contentHtml: html })
2021
});
2122
};
2223
module.exports = sendEmail;

src/services/emailTemplates.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const escapeHtml = (value) => {
2+
if (value === null || value === undefined) return "";
3+
return String(value)
4+
.replaceAll("&", "&amp;")
5+
.replaceAll("<", "&lt;")
6+
.replaceAll(">", "&gt;")
7+
.replaceAll('"', "&quot;")
8+
.replaceAll("'", "&#039;");
9+
};
10+
11+
const button = (url, label) => {
12+
const safeUrl = escapeHtml(url);
13+
const safeLabel = escapeHtml(label);
14+
15+
return `
16+
<div style="margin: 20px 0; text-align: center;">
17+
<a href="${safeUrl}" target="_blank" rel="noopener noreferrer"
18+
style="display: inline-block; background: #111827; color: #ffffff; text-decoration: none; padding: 12px 18px; border-radius: 8px; font-weight: 600;">
19+
${safeLabel}
20+
</a>
21+
</div>
22+
`;
23+
};
24+
25+
const wrapEmail = ({ subject, preheader, contentHtml }) => {
26+
const safeSubject = escapeHtml(subject || "DevSync");
27+
const safePreheader = escapeHtml(preheader || subject || "");
28+
29+
// Table-based layout for broad email client support.
30+
return `
31+
<!doctype html>
32+
<html lang="en">
33+
<head>
34+
<meta charset="utf-8" />
35+
<meta name="viewport" content="width=device-width,initial-scale=1" />
36+
<meta name="x-apple-disable-message-reformatting" />
37+
<title>${safeSubject}</title>
38+
</head>
39+
<body style="margin:0; padding:0; background:#f3f4f6;">
40+
<div style="display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;">
41+
${safePreheader}
42+
</div>
43+
44+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:#f3f4f6; padding: 24px 12px;">
45+
<tr>
46+
<td align="center">
47+
<table role="presentation" width="600" cellspacing="0" cellpadding="0" style="width:600px; max-width:100%; background:#ffffff; border-radius: 12px; overflow:hidden;">
48+
<tr>
49+
<td style="padding: 18px 20px; background:#111827; color:#ffffff; font-family: Arial, Helvetica, sans-serif;">
50+
<div style="font-size: 16px; font-weight: 700; letter-spacing: 0.2px;">DevSync</div>
51+
<div style="font-size: 12px; opacity: 0.85; margin-top: 4px;">${safeSubject}</div>
52+
</td>
53+
</tr>
54+
55+
<tr>
56+
<td style="padding: 22px 20px; font-family: Arial, Helvetica, sans-serif; color:#111827; font-size: 14px; line-height: 1.6;">
57+
${contentHtml || ""}
58+
</td>
59+
</tr>
60+
61+
<tr>
62+
<td style="padding: 16px 20px; background:#f9fafb; font-family: Arial, Helvetica, sans-serif; color:#6b7280; font-size: 12px; line-height: 1.5;">
63+
<div>You’re receiving this email because it relates to your DevSync account.</div>
64+
<div style="margin-top: 6px;">If you didn’t expect this, you can ignore this message.</div>
65+
</td>
66+
</tr>
67+
</table>
68+
69+
<div style="font-family: Arial, Helvetica, sans-serif; color:#9ca3af; font-size: 12px; margin-top: 12px;">© ${new Date().getFullYear()} DevSync</div>
70+
</td>
71+
</tr>
72+
</table>
73+
</body>
74+
</html>
75+
`.trim();
76+
};
77+
78+
module.exports = {
79+
escapeHtml,
80+
button,
81+
wrapEmail,
82+
};

src/utils/cronScheduleEmail.js

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,99 @@
1-
// const cron = require("node-cron");
2-
// const { subDays, startOfDay, endOfDay } = require("date-fns")
3-
// const ConnectionRequest = require("../models/connectionRequest")
4-
// const sendEmail = require("./sendEmail")
5-
6-
// const yesterDay = subDays(new Date(), 0);
7-
// const start = startOfDay(yesterDay);
8-
// const end = endOfDay(yesterDay);
9-
10-
// cron.schedule("0 8 1 1 1 ", async () => {
11-
// try {
12-
// const pendingRequests =await ConnectionRequest.find({
13-
// status: "interested",
14-
// createdAt: {
15-
// $gte: start,
16-
// $lt: end
17-
// }
18-
// }).populate("fromUserId toUserId")
19-
20-
// const emails = [...new Set(pendingRequests.map((req) => req.toUserId.email))]
21-
// console.log(emails);
22-
23-
// for (const email of emails) {
24-
// try {
25-
// const res = await sendEmail(email, "Pending Requests", "You have connection requests pending so please review them", "<h1>Review Requests</h1>");
26-
// console.log(res);
27-
// } catch (error) {
28-
// console.log(error)
29-
// }
30-
// }
31-
// } catch (error) {
32-
// console.error(error);
33-
// }
34-
// })
1+
const cron = require("node-cron");
2+
const mongoose = require("mongoose");
3+
const { subDays, startOfDay, endOfDay, format } = require("date-fns");
4+
const ConnectionRequest = require("../models/connectionRequest");
5+
const sendEmail = require("../services/emailService");
6+
const { escapeHtml, button } = require("../services/emailTemplates");
7+
8+
const CRON_EXPRESSION = "0 8 * * *"; // Every day at 08:00
9+
const CRON_TIMEZONE = process.env.CRON_TIMEZONE; // optional, e.g. "Asia/Kolkata"
10+
11+
const groupBy = (items, getKey) => {
12+
return items.reduce((acc, item) => {
13+
const key = getKey(item);
14+
if (!acc[key]) acc[key] = [];
15+
acc[key].push(item);
16+
return acc;
17+
}, {});
18+
};
19+
20+
cron.schedule(
21+
CRON_EXPRESSION,
22+
async () => {
23+
try {
24+
if (mongoose.connection.readyState !== 1) {
25+
console.warn("[cron] DB not connected; skipping daily request digest");
26+
return;
27+
}
28+
29+
const yesterday = subDays(new Date(), 1);
30+
const start = startOfDay(yesterday);
31+
const end = endOfDay(yesterday);
32+
33+
const requests = await ConnectionRequest.find({
34+
status: "interested",
35+
createdAt: { $gte: start, $lt: end },
36+
})
37+
.populate("fromUserId", "firstName lastName")
38+
.populate("toUserId", "firstName lastName email");
39+
40+
if (!requests.length) return;
41+
42+
const byRecipient = groupBy(
43+
requests.filter(r => r?.toUserId?.email),
44+
(r) => String(r.toUserId._id)
45+
);
46+
47+
const dateLabel = format(yesterday, "do MMM yyyy");
48+
const appUrl = (process.env.FRONTEND_URL || "").replace(/\/$/, "");
49+
const cta = appUrl ? button(appUrl, "Open DevSync") : "";
50+
51+
for (const recipientId of Object.keys(byRecipient)) {
52+
const recipientRequests = byRecipient[recipientId];
53+
const recipient = recipientRequests[0].toUserId;
54+
const to = recipient.email;
55+
56+
const listItems = recipientRequests
57+
.map((r) => {
58+
const from = r.fromUserId;
59+
const fromName = escapeHtml(
60+
[from?.firstName, from?.lastName].filter(Boolean).join(" ") || "Someone"
61+
);
62+
return `<li style="margin: 6px 0;">${fromName} sent you a connection request</li>`;
63+
})
64+
.join("");
65+
66+
const count = recipientRequests.length;
67+
const subject = `You have ${count} new connection request${count === 1 ? "" : "s"} on DevSync`;
68+
69+
const recipientName = escapeHtml(recipient.firstName || "there");
70+
71+
const html = `
72+
<p style="margin: 0 0 12px;">Hi ${recipientName},</p>
73+
<p style="margin: 0 0 12px;">Here’s a summary of the connection requests you received on <b>${escapeHtml(dateLabel)}</b>:</p>
74+
<ul style="padding-left: 18px; margin: 0 0 12px;">${listItems}</ul>
75+
<p style="margin: 0 0 12px;">Please review and respond when you’re ready.</p>
76+
${cta}
77+
<p style="margin: 12px 0 0; color: #6b7280; font-size: 12px;">If the button doesn’t work, open: ${escapeHtml(appUrl || "DevSync")}</p>
78+
`;
79+
80+
try {
81+
await sendEmail({
82+
to,
83+
subject,
84+
preheader: `You received ${count} new request${count === 1 ? "" : "s"} yesterday.`,
85+
html,
86+
});
87+
} catch (error) {
88+
console.error("[cron] Failed to send digest to", to, error?.message || error);
89+
}
90+
}
91+
} catch (error) {
92+
console.error("[cron] Daily request digest failed:", error?.message || error);
93+
}
94+
},
95+
CRON_TIMEZONE ? { timezone: CRON_TIMEZONE } : undefined
96+
);
97+
3598

3699

0 commit comments

Comments
 (0)