diff --git a/App/FeatureSet/AdminDashboard/src/App.tsx b/App/FeatureSet/AdminDashboard/src/App.tsx
index 82da82651c..17e76498ef 100644
--- a/App/FeatureSet/AdminDashboard/src/App.tsx
+++ b/App/FeatureSet/AdminDashboard/src/App.tsx
@@ -12,6 +12,7 @@ import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from "./Pages/Settings/Probes/Index";
import SettingsAIAgents from "./Pages/Settings/AIAgents/Index";
import SettingsLlmProviders from "./Pages/Settings/LlmProviders/Index";
+import SendEmail from "./Pages/SendEmail/Index";
import Users from "./Pages/Users/Index";
import PageMap from "./Utils/PageMap";
import RouteMap from "./Utils/RouteMap";
@@ -149,6 +150,11 @@ const App: () => JSX.Element = () => {
path={RouteMap[PageMap.SETTINGS_DATA_RETENTION]?.toString() || ""}
element={}
/>
+
+ }
+ />
);
diff --git a/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx b/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx
index d024c0eab3..4b5fb95507 100644
--- a/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx
+++ b/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx
@@ -2,7 +2,10 @@ import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
-import NavBar, { NavItem } from "Common/UI/Components/Navbar/NavBar";
+import NavBar, {
+ MoreMenuItem,
+ NavItem,
+} from "Common/UI/Components/Navbar/NavBar";
import React, { FunctionComponent, ReactElement } from "react";
const DashboardNavbar: FunctionComponent = (): ReactElement => {
@@ -28,7 +31,21 @@ const DashboardNavbar: FunctionComponent = (): ReactElement => {
},
];
- return ;
+ const moreMenuItems: MoreMenuItem[] = [
+ {
+ title: "Send Email",
+ description: "Send announcement emails to all registered users.",
+ icon: IconProp.Email,
+ route: RouteUtil.populateRouteParams(
+ RouteMap[PageMap.SEND_EMAIL] as Route,
+ ),
+ activeRoute: RouteUtil.populateRouteParams(
+ RouteMap[PageMap.SEND_EMAIL] as Route,
+ ),
+ },
+ ];
+
+ return ;
};
export default DashboardNavbar;
diff --git a/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx
new file mode 100644
index 0000000000..169cde6ca4
--- /dev/null
+++ b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx
@@ -0,0 +1,282 @@
+import PageMap from "../../Utils/PageMap";
+import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
+import Route from "Common/Types/API/Route";
+import React, { FunctionComponent, ReactElement, useState } from "react";
+import Page from "Common/UI/Components/Page/Page";
+import Card from "Common/UI/Components/Card/Card";
+import BasicForm from "Common/UI/Components/Forms/BasicForm";
+import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
+import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
+import { JSONObject } from "Common/Types/JSON";
+import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
+import HTTPResponse from "Common/Types/API/HTTPResponse";
+import URL from "Common/Types/API/URL";
+import API from "Common/UI/Utils/API/API";
+import { APP_API_URL } from "Common/UI/Config";
+import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
+
+const SendEmail: FunctionComponent = (): ReactElement => {
+ const [isSendingTest, setIsSendingTest] = useState(false);
+ const [isSendingAll, setIsSendingAll] = useState(false);
+ const [testError, setTestError] = useState("");
+ const [testSuccess, setTestSuccess] = useState("");
+ const [sendAllError, setSendAllError] = useState("");
+ const [sendAllSuccess, setSendAllSuccess] = useState("");
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [pendingSubject, setPendingSubject] = useState("");
+ const [pendingMessage, setPendingMessage] = useState("");
+
+ const sendToAllUsers: () => Promise = async (): Promise => {
+ setIsSendingAll(true);
+ setSendAllError("");
+ setSendAllSuccess("");
+
+ try {
+ const response: HTTPResponse | HTTPErrorResponse =
+ await API.post({
+ url: URL.fromString(APP_API_URL.toString()).addRoute(
+ "/notification/broadcast-email/send-to-all-users",
+ ),
+ data: {
+ subject: pendingSubject,
+ message: pendingMessage,
+ },
+ });
+
+ if (response instanceof HTTPErrorResponse) {
+ throw response;
+ }
+
+ if (response.isFailure()) {
+ throw new Error("Failed to send emails.");
+ }
+
+ const data: JSONObject = response.data as JSONObject;
+ setSendAllSuccess(
+ `Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`,
+ );
+ } catch (err) {
+ setSendAllError(API.getFriendlyMessage(err));
+ } finally {
+ setIsSendingAll(false);
+ }
+ };
+
+ return (
+
+
+ {testSuccess ? (
+
+ ) : (
+ <>>
+ )}
+
+ void,
+ ) => {
+ const subject: string = String(values["subject"] || "").trim();
+ const message: string = String(values["message"] || "").trim();
+ const testEmail: string = String(values["testEmail"] || "").trim();
+
+ if (!subject || !message || !testEmail) {
+ setTestSuccess("");
+ setTestError("Please fill in all fields.");
+ return;
+ }
+
+ setIsSendingTest(true);
+ setTestError("");
+ setTestSuccess("");
+
+ try {
+ const response: HTTPResponse | HTTPErrorResponse =
+ await API.post({
+ url: URL.fromString(APP_API_URL.toString()).addRoute(
+ "/notification/broadcast-email/send-test",
+ ),
+ data: {
+ subject,
+ message,
+ testEmail,
+ },
+ });
+
+ if (response instanceof HTTPErrorResponse) {
+ throw response;
+ }
+
+ if (response.isFailure()) {
+ throw new Error("Failed to send test email.");
+ }
+
+ setTestSuccess(
+ "Test email sent successfully. Please check your inbox.",
+ );
+
+ if (onSubmitSuccessful) {
+ onSubmitSuccessful();
+ }
+ } catch (err) {
+ setTestError(API.getFriendlyMessage(err));
+ } finally {
+ setIsSendingTest(false);
+ }
+ }}
+ />
+
+
+
+ {sendAllSuccess ? (
+
+ ) : (
+ <>>
+ )}
+
+ {
+ const subject: string = String(values["subject"] || "").trim();
+ const message: string = String(values["message"] || "").trim();
+
+ if (!subject || !message) {
+ setSendAllSuccess("");
+ setSendAllError("Please fill in all fields.");
+ return;
+ }
+
+ setPendingSubject(subject);
+ setPendingMessage(message);
+ setShowConfirmModal(true);
+ }}
+ />
+
+
+ {showConfirmModal ? (
+ {
+ setShowConfirmModal(false);
+ await sendToAllUsers();
+ }}
+ onClose={() => {
+ setShowConfirmModal(false);
+ }}
+ />
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+export default SendEmail;
diff --git a/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts b/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts
index 7b122e2418..0a99cc71bd 100644
--- a/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts
+++ b/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts
@@ -23,6 +23,8 @@ enum PageMap {
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = "SETTINGS_API_KEY",
SETTINGS_DATA_RETENTION = "SETTINGS_DATA_RETENTION",
+
+ SEND_EMAIL = "SEND_EMAIL",
}
export default PageMap;
diff --git a/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts b/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts
index 0e7393c07a..e07e8765b4 100644
--- a/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts
+++ b/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts
@@ -39,6 +39,8 @@ const RouteMap: Dictionary = {
[PageMap.SETTINGS_DATA_RETENTION]: new Route(
`/admin/settings/data-retention`,
),
+
+ [PageMap.SEND_EMAIL]: new Route(`/admin/send-email`),
};
export class RouteUtil {
diff --git a/App/FeatureSet/Notification/API/BroadcastEmail.ts b/App/FeatureSet/Notification/API/BroadcastEmail.ts
new file mode 100644
index 0000000000..fa9373ea30
--- /dev/null
+++ b/App/FeatureSet/Notification/API/BroadcastEmail.ts
@@ -0,0 +1,135 @@
+import MailService from "../Services/MailService";
+import Email from "Common/Types/Email";
+import EmailMessage from "Common/Types/Email/EmailMessage";
+import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
+import BadDataException from "Common/Types/Exception/BadDataException";
+import { JSONObject } from "Common/Types/JSON";
+import LIMIT_MAX from "Common/Types/Database/LimitMax";
+import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
+import UserService from "Common/Server/Services/UserService";
+import Express, {
+ ExpressRequest,
+ ExpressResponse,
+ ExpressRouter,
+ NextFunction,
+} from "Common/Server/Utils/Express";
+import Response from "Common/Server/Utils/Response";
+import logger from "Common/Server/Utils/Logger";
+import User from "Common/Models/DatabaseModels/User";
+
+const router: ExpressRouter = Express.getRouter();
+
+router.post(
+ "/send-test",
+ ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
+ try {
+ const body: JSONObject = req.body;
+
+ const subject: string = body["subject"] as string;
+ const message: string = body["message"] as string;
+ const testEmail: string = body["testEmail"] as string;
+
+ if (!subject) {
+ throw new BadDataException("Subject is required");
+ }
+
+ if (!message) {
+ throw new BadDataException("Message is required");
+ }
+
+ if (!testEmail) {
+ throw new BadDataException("Test email address is required");
+ }
+
+ const mail: EmailMessage = {
+ templateType: EmailTemplateType.SimpleMessage,
+ toEmail: new Email(testEmail),
+ subject: subject,
+ vars: {
+ subject: subject,
+ message: message,
+ },
+ body: "",
+ };
+
+ await MailService.send(mail);
+
+ return Response.sendEmptySuccessResponse(req, res);
+ } catch (err) {
+ return next(err);
+ }
+ },
+);
+
+router.post(
+ "/send-to-all-users",
+ ClusterKeyAuthorization.isAuthorizedServiceMiddleware,
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
+ try {
+ const body: JSONObject = req.body;
+
+ const subject: string = body["subject"] as string;
+ const message: string = body["message"] as string;
+
+ if (!subject) {
+ throw new BadDataException("Subject is required");
+ }
+
+ if (!message) {
+ throw new BadDataException("Message is required");
+ }
+
+ const users: Array = await UserService.findAllBy({
+ query: {},
+ select: {
+ email: true,
+ },
+ skip: 0,
+ props: {
+ isRoot: true,
+ },
+ });
+
+ let sentCount: number = 0;
+ let errorCount: number = 0;
+
+ for (const user of users) {
+ if (!user.email) {
+ continue;
+ }
+
+ try {
+ const mail: EmailMessage = {
+ templateType: EmailTemplateType.SimpleMessage,
+ toEmail: user.email,
+ subject: subject,
+ vars: {
+ subject: subject,
+ message: message,
+ },
+ body: "",
+ };
+
+ await MailService.send(mail);
+ sentCount++;
+ } catch (err) {
+ errorCount++;
+ logger.error(
+ `Failed to send broadcast email to ${user.email.toString()}: ${err}`,
+ );
+ }
+ }
+
+ return Response.sendJsonObjectResponse(req, res, {
+ totalUsers: users.length,
+ sentCount: sentCount,
+ errorCount: errorCount,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ },
+);
+
+export default router;
diff --git a/App/FeatureSet/Notification/Index.ts b/App/FeatureSet/Notification/Index.ts
index c78ebdaebb..c4915d714d 100644
--- a/App/FeatureSet/Notification/Index.ts
+++ b/App/FeatureSet/Notification/Index.ts
@@ -1,3 +1,4 @@
+import BroadcastEmailAPI from "./API/BroadcastEmail";
import CallAPI from "./API/Call";
// API
import MailAPI from "./API/Mail";
@@ -27,6 +28,10 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI);
app.use([`/${APP_NAME}/incoming-call`, "/incoming-call"], IncomingCallAPI);
+ app.use(
+ [`/${APP_NAME}/broadcast-email`, "/broadcast-email"],
+ BroadcastEmailAPI,
+ );
},
};