diff --git a/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx b/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx
new file mode 100644
index 00000000..aa6f6b9b
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { useState } from "react";
+import { SesSetting } from "@prisma/client";
+import { Trash } from "lucide-react";
+
+import { Button } from "@usesend/ui/src/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@usesend/ui/src/dialog";
+import { toast } from "@usesend/ui/src/toaster";
+
+import { api } from "~/trpc/react";
+
+export default function DeleteSesConfiguration({
+ setting,
+}: {
+ setting: SesSetting;
+}) {
+ const [open, setOpen] = useState(false);
+
+ const deleteSesSettings = api.admin.deleteSesSettings.useMutation();
+ const utils = api.useUtils();
+
+ const handleDelete = () => {
+ deleteSesSettings.mutate(
+ { settingsId: setting.id },
+ {
+ onSuccess: () => {
+ utils.admin.invalidate();
+ toast.success("SES configuration deleted", {
+ description: `${setting.region} has been removed`,
+ });
+ setOpen(false);
+ },
+ onError: (error) => {
+ toast.error("Failed to delete SES configuration", {
+ description: error.message,
+ });
+ },
+ }
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx
index 6d785b64..69b1761c 100644
--- a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx
+++ b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx
@@ -13,6 +13,7 @@ import { api } from "~/trpc/react";
import Spinner from "@usesend/ui/src/spinner";
import EditSesConfiguration from "./edit-ses-configuration";
import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy";
+import DeleteSesConfiguration from "./delete-ses-configuration";
export default function SesConfigurations() {
const sesSettingsQuery = api.admin.getSesSettings.useQuery();
@@ -71,7 +72,10 @@ export default function SesConfigurations() {
{sesSetting.sesEmailRateLimit}
{sesSetting.transactionalQuota}%
-
+
+
+
+
))
diff --git a/apps/web/src/server/api/routers/admin.ts b/apps/web/src/server/api/routers/admin.ts
index f6ec36ab..98db8491 100644
--- a/apps/web/src/server/api/routers/admin.ts
+++ b/apps/web/src/server/api/routers/admin.ts
@@ -122,6 +122,17 @@ export const adminRouter = createTRPCRouter({
});
}),
+ deleteSesSettings: adminProcedure
+ .input(
+ z.object({
+ settingsId: z.string(),
+ })
+ )
+ .mutation(async ({ input }) => {
+ await SesSettingsService.deleteSesSetting(input.settingsId);
+ return true;
+ }),
+
getSetting: adminProcedure
.input(
z.object({
diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts
index 1b3296b8..b1a807c9 100644
--- a/apps/web/src/server/aws/ses.ts
+++ b/apps/web/src/server/aws/ses.ts
@@ -7,6 +7,7 @@ import {
SendEmailCommand,
CreateConfigurationSetEventDestinationCommand,
CreateConfigurationSetCommand,
+ DeleteConfigurationSetCommand,
EventType,
GetAccountCommand,
CreateTenantResourceAssociationCommand,
@@ -310,3 +311,26 @@ export async function addWebhookConfiguration(
const response = await sesClient.send(command);
return response.$metadata.httpStatusCode === 200;
}
+
+export async function deleteConfigurationSet(
+ configName: string,
+ region: string
+) {
+ const sesClient = getSesClient(region);
+ const command = new DeleteConfigurationSetCommand({
+ ConfigurationSetName: configName,
+ });
+
+ try {
+ await sesClient.send(command);
+ } catch (error: any) {
+ if (error?.name === "NotFoundException") {
+ logger.warn(
+ { configName, region },
+ "Configuration set not found while deleting"
+ );
+ return;
+ }
+ throw error;
+ }
+}
diff --git a/apps/web/src/server/service/email-queue-service.ts b/apps/web/src/server/service/email-queue-service.ts
index 730eedbb..e1831eda 100644
--- a/apps/web/src/server/service/email-queue-service.ts
+++ b/apps/web/src/server/service/email-queue-service.ts
@@ -108,6 +108,27 @@ export class EmailQueueService {
}
}
+ public static async removeRegion(region: string) {
+ logger.info({ region }, `[EmailQueueService]: Removing queues for region`);
+
+ const transactionalQueue = this.transactionalQueue.get(region);
+ const transactionalWorker = this.transactionalWorker.get(region);
+ const marketingQueue = this.marketingQueue.get(region);
+ const marketingWorker = this.marketingWorker.get(region);
+
+ await Promise.all([
+ transactionalWorker?.close(),
+ transactionalQueue?.close(),
+ marketingWorker?.close(),
+ marketingQueue?.close(),
+ ]);
+
+ this.transactionalQueue.delete(region);
+ this.transactionalWorker.delete(region);
+ this.marketingQueue.delete(region);
+ this.marketingWorker.delete(region);
+ }
+
public static async queueEmail(
emailId: string,
teamId: number,
diff --git a/apps/web/src/server/service/ses-settings-service.ts b/apps/web/src/server/service/ses-settings-service.ts
index 7402d713..d576fba6 100644
--- a/apps/web/src/server/service/ses-settings-service.ts
+++ b/apps/web/src/server/service/ses-settings-service.ts
@@ -202,6 +202,43 @@ export class SesSettingsService {
await this.invalidateCache();
}
+ public static async deleteSesSetting(id: string) {
+ await this.checkInitialized();
+
+ const setting = await db.sesSetting.findUnique({
+ where: { id },
+ });
+
+ if (!setting) {
+ throw new Error("SES setting not found");
+ }
+
+ const configNames = [
+ setting.configGeneral,
+ setting.configClick,
+ setting.configOpen,
+ setting.configFull,
+ ].filter(Boolean) as string[];
+
+ await Promise.all(
+ configNames.map((configName) =>
+ ses.deleteConfigurationSet(configName, setting.region)
+ )
+ );
+
+ if (setting.topicArn) {
+ await sns.deleteTopic(setting.topicArn, setting.region);
+ }
+
+ await EmailQueueService.removeRegion(setting.region);
+
+ await db.sesSetting.delete({
+ where: { id },
+ });
+
+ await this.invalidateCache();
+ }
+
public static async checkInitialized() {
if (!this.initialized) {
await this.invalidateCache();