feat:add email send functionality

This commit is contained in:
ARJUN S THAMPI
2026-03-19 13:12:04 +05:30
parent 1bbf7f9c1c
commit 834eaad3c3
17 changed files with 759 additions and 6 deletions

View File

@@ -21,6 +21,7 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"postmark": "^4.0.7",
"prisma": "^6.19.2"
},
"devDependencies": {
@@ -211,6 +212,23 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -482,6 +500,18 @@
"integrity": "sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -601,6 +631,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -715,6 +754,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -879,6 +933,63 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1022,6 +1133,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1294,9 +1420,9 @@
}
},
"node_modules/minimatch": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -1586,6 +1712,15 @@
"pathe": "^2.0.3"
}
},
"node_modules/postmark": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.7.tgz",
"integrity": "sha512-DjNniUl1XNCGUKhCR98ePd5gv16rlUAVKKaU9TUqnE3hDSqfT9XDulu1idjagQmdyGscqnRtXk/puAEiYMeevg==",
"license": "MIT",
"dependencies": {
"axios": "^1.13.5"
}
},
"node_modules/prisma": {
"version": "6.19.2",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
@@ -1625,6 +1760,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -27,6 +27,7 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"postmark": "^4.0.7",
"prisma": "^6.19.2"
},
"devDependencies": {

View File

@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "EmailConfig" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"type" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "EmailConfig_pkey" PRIMARY KEY ("id")
);

View File

@@ -173,4 +173,16 @@ model AcademicsResearch {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model EmailConfig {
id Int @id @default(autoincrement())
name String
email String
type String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -12,6 +12,7 @@ import candidateRoutes from "./routes/candidate.routes.js";
import appointmentRoutes from "./routes/appointment.routes.js";
import inquiryRoutes from "./routes/inquiry.routes.js";
import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
import emailConfigRoutes from "./routes/emailConfig.routes.js";
dotenv.config();
@@ -47,6 +48,7 @@ app.use("/api/candidates", candidateRoutes);
app.use("/api/appointments", appointmentRoutes);
app.use("/api/inquiry", inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes);
app.use("/api/email", emailConfigRoutes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {

View File

@@ -1,5 +1,6 @@
import prisma from "../prisma/client.js";
//CREATE APPOINTMENT
import {sendEmail} from "../utils/sendEmail.js";
import {getEmailsByType} from "../utils/getEmailByTypes.js";
export const createAppointment = async (req, res) => {
try {
@@ -29,6 +30,29 @@ export const createAppointment = async (req, res) => {
},
});
try {
const emailList = await getEmailsByType("APPOINTMENT");
if (emailList) {
await sendEmail({
to: emailList,
subject: "New Appointment Booked",
html: `
<h2>New Appointment Booked</h2>
<p><b>Name:</b> ${name}</p>
<p><b>Phone:</b> ${mobileNumber}</p>
<p><b>Email:</b> ${email || "-"}</p>
<p><b>Doctor:</b> ${appointment.doctor?.name}</p>
<p><b>Department:</b> ${appointment.department?.name}</p>
<p><b>Date:</b> ${new Date(date).toLocaleDateString()}</p>
<p><b>Message:</b> ${message || "-"}</p>
`,
});
}
} catch (err) {
console.error("Email failed:", err);
}
res.status(201).json({
success: true,
message: "Appointment booked successfully",

View File

@@ -0,0 +1,139 @@
import prisma from "../prisma/client.js";
// CREATE
export const createEmailConfig = async (req, res) => {
try {
const {name, email, type, isActive} = req.body;
if (!name || !email || !type) {
return res.status(400).json({
success: false,
message: "Name, Email and Type are required",
});
}
const newEmail = await prisma.emailConfig.create({
data: {
name,
email,
type,
isActive: isActive ?? true,
},
});
res.status(201).json({
success: true,
message: "Email config created",
data: newEmail,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create email config",
});
}
};
//GET ALL
export const getEmailConfigs = async (req, res) => {
try {
const emails = await prisma.emailConfig.findMany({
orderBy: {
createdAt: "desc",
},
});
res.status(200).json({
success: true,
data: emails,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch email configs",
});
}
};
// GET SINGLE
export const getEmailConfig = async (req, res) => {
try {
const {id} = req.params;
const email = await prisma.emailConfig.findUnique({
where: {
id: Number(id),
},
});
if (!email) {
return res.status(404).json({
success: false,
message: "Email config not found",
});
}
res.status(200).json({
success: true,
data: email,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch email config",
});
}
};
// UPDATE
export const updateEmailConfig = async (req, res) => {
try {
const {id} = req.params;
const updated = await prisma.emailConfig.update({
where: {
id: Number(id),
},
data: req.body,
});
res.status(200).json({
success: true,
message: "Email config updated",
data: updated,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update email config",
});
}
};
// DELETE
export const deleteEmailConfig = async (req, res) => {
try {
const {id} = req.params;
await prisma.emailConfig.delete({
where: {
id: Number(id),
},
});
res.status(200).json({
success: true,
message: "Email config deleted",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete email config",
});
}
};

View File

@@ -0,0 +1,19 @@
import express from "express";
import {
getEmailConfigs,
createEmailConfig,
updateEmailConfig,
deleteEmailConfig,
} from "../controllers/emailConfig.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.get("/getAll", getEmailConfigs);
router.post("/", jwtAuthMiddleware, createEmailConfig);
router.put("/:id", jwtAuthMiddleware, updateEmailConfig);
router.delete("/:id", jwtAuthMiddleware, deleteEmailConfig);
export default router;

View File

@@ -0,0 +1,17 @@
import prisma from "../prisma/client.js";
export const getEmailsByType = async (type) => {
try {
const emails = await prisma.emailConfig.findMany({
where: {
type,
isActive: true,
},
});
return emails.map((e) => e.email).join(",");
} catch (error) {
console.error("Fetch email config error:", error);
return "";
}
};

View File

@@ -0,0 +1,18 @@
import postmark from "postmark";
const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
export const sendEmail = async ({to, subject, html, text}) => {
try {
await client.sendEmail({
From: process.env.EMAIL_FROM,
To: to,
Subject: subject,
HtmlBody: html,
TextBody: text || "",
MessageStream: "outbound",
});
} catch (error) {
console.error("Email send error:", error);
}
};