diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 1b3296b8..0fda94c8 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -11,6 +11,7 @@ import { GetAccountCommand, CreateTenantResourceAssociationCommand, DeleteTenantResourceAssociationCommand, + DeleteSuppressedDestinationCommand, } from "@aws-sdk/client-sesv2"; import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts"; import { generateKeyPairSync } from "crypto"; @@ -310,3 +311,36 @@ export async function addWebhookConfiguration( const response = await sesClient.send(command); return response.$metadata.httpStatusCode === 200; } + +/** + * Remove email from AWS SES account-level suppression list + * Returns true if successful or email wasn't suppressed, false on error + */ +export async function deleteFromSesSuppressionList( + email: string, + region: string +): Promise { + const sesClient = getSesClient(region); + try { + const command = new DeleteSuppressedDestinationCommand({ + EmailAddress: email, + }); + await sesClient.send(command); + logger.info({ email, region }, "Removed email from SES suppression list"); + return true; + } catch (error: any) { + // NotFoundException means email wasn't in SES suppression list - that's fine + if (error.name === "NotFoundException") { + logger.debug( + { email, region }, + "Email not in SES suppression list (already removed or never added)" + ); + return true; + } + logger.error( + { email, region, error: error.message }, + "Failed to remove email from SES suppression list" + ); + return false; + } +} diff --git a/apps/web/src/server/service/suppression-service.ts b/apps/web/src/server/service/suppression-service.ts index 16c59e12..5c7d1468 100644 --- a/apps/web/src/server/service/suppression-service.ts +++ b/apps/web/src/server/service/suppression-service.ts @@ -2,6 +2,7 @@ import { SuppressionReason, SuppressionList } from "@prisma/client"; import { db } from "../db"; import { UnsendApiError } from "~/server/public-api/api-error"; import { logger } from "../logger/log"; +import { deleteFromSesSuppressionList } from "../aws/ses"; export type AddSuppressionParams = { email: string; @@ -120,22 +121,71 @@ export class SuppressionService { } /** - * Remove email from suppression list + * Remove email from suppression list (both local DB and AWS SES) */ static async removeSuppression(email: string, teamId: number): Promise { + const normalizedEmail = email.toLowerCase().trim(); + + // Get all unique regions from team's domains for AWS SES cleanup + try { + const teamDomains = await db.domain.findMany({ + where: { teamId }, + select: { region: true }, + }); + const uniqueRegions = [...new Set(teamDomains.map((d) => d.region))]; + + // Attempt to remove from AWS SES in all regions (best effort, don't throw) + if (uniqueRegions.length > 0) { + const results = await Promise.allSettled( + uniqueRegions.map((region) => + deleteFromSesSuppressionList(normalizedEmail, region) + ) + ); + + // Check for failures - deleteFromSesSuppressionList returns false on error + const failures = results.filter( + (r) => + r.status === "rejected" || + (r.status === "fulfilled" && r.value === false) + ); + if (failures.length > 0) { + logger.warn( + { + email: normalizedEmail, + teamId, + failedRegions: failures.length, + totalRegions: uniqueRegions.length, + }, + "Some AWS SES regions failed during suppression removal" + ); + } + } + } catch (error) { + // AWS SES cleanup failure should not block local DB deletion + logger.error( + { + email: normalizedEmail, + teamId, + error: error instanceof Error ? error.message : "Unknown error", + }, + "Failed to cleanup AWS SES suppression (continuing with local deletion)" + ); + } + + // Delete from local database try { const deleted = await db.suppressionList.delete({ where: { teamId_email: { teamId, - email: email.toLowerCase().trim(), + email: normalizedEmail, }, }, }); logger.info( { - email, + email: normalizedEmail, teamId, suppressionId: deleted.id, }, @@ -149,7 +199,7 @@ export class SuppressionService { ) { logger.debug( { - email, + email: normalizedEmail, teamId, }, "Attempted to remove non-existent suppression - already not suppressed" @@ -159,7 +209,7 @@ export class SuppressionService { logger.error( { - email, + email: normalizedEmail, teamId, error: error instanceof Error ? error.message : "Unknown error", },