From 9e60a9fa2bcbb3ad283f2c3ed64858ee134f0e36 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 1 Mar 2026 17:08:58 -0500 Subject: [PATCH 1/2] updates and cleanup --- app/api/e2/emails/reply.ts | 20 +- app/api/e2/emails/send.ts | 20 +- app/api/e2/lib/auth.ts | 4 +- .../webhooks/send-email/build-ses-command.ts | 45 +++ .../send-email/resolve-tenant-info.ts | 73 ++++ app/api/webhooks/send-email/route.ts | 371 ++---------------- bun.lockb | Bin 618700 -> 661782 bytes features/settings/hooks/useApiKeyMutations.ts | 9 +- features/settings/hooks/useApiKeysQuery.ts | 30 +- lib/auth/auth-client.ts | 16 +- lib/auth/auth.ts | 8 +- lib/auth/v2-auth.ts | 338 ++++++++-------- lib/aws-ses/ses-client.ts | 48 +++ .../agent-email-helper.test.ts | 47 +++ lib/email-management/agent-email-helper.ts | 72 +--- .../delivery-event-tracker.test.ts | 69 ++++ .../delivery-event-tracker.ts | 14 +- lib/email-management/dsn-parser.test.ts | 54 ++- lib/email-management/email-blocking.ts | 18 +- lib/email-management/email-forwarder.ts | 7 +- lib/email-management/email-parser.test.ts | 142 +++++++ lib/email-management/email-router.ts | 133 +------ .../email-thread-parser.test.ts | 199 ++++++++++ .../sending-spike-detector.test.ts | 101 +++++ .../sending-spike-detector.ts | 8 +- lib/email-management/warmup-limits.test.ts | 56 +++ lib/email-management/warmup-limits.ts | 2 +- lib/email-management/webhook-payload.ts | 150 +++++++ lib/guard/rule-matcher.test.ts | 131 +++++++ lib/guard/rule-matcher.ts | 4 +- lib/ses-monitoring/rate-tracker.ts | 3 +- lib/utils/attachment-utils.test.ts | 94 +++++ lib/utils/attachment-utils.ts | 82 ++++ lib/utils/email-utils.test.ts | 95 +++++ lib/utils/email-utils.ts | 57 +++ package.json | 15 +- 36 files changed, 1760 insertions(+), 775 deletions(-) create mode 100644 app/api/webhooks/send-email/build-ses-command.ts create mode 100644 app/api/webhooks/send-email/resolve-tenant-info.ts create mode 100644 lib/aws-ses/ses-client.ts create mode 100644 lib/email-management/agent-email-helper.test.ts create mode 100644 lib/email-management/delivery-event-tracker.test.ts create mode 100644 lib/email-management/email-parser.test.ts create mode 100644 lib/email-management/email-thread-parser.test.ts create mode 100644 lib/email-management/sending-spike-detector.test.ts create mode 100644 lib/email-management/warmup-limits.test.ts create mode 100644 lib/email-management/webhook-payload.ts create mode 100644 lib/guard/rule-matcher.test.ts create mode 100644 lib/utils/attachment-utils.test.ts create mode 100644 lib/utils/attachment-utils.ts create mode 100644 lib/utils/email-utils.test.ts create mode 100644 lib/utils/email-utils.ts diff --git a/app/api/e2/emails/reply.ts b/app/api/e2/emails/reply.ts index 2780654a..a52cc3cb 100644 --- a/app/api/e2/emails/reply.ts +++ b/app/api/e2/emails/reply.ts @@ -1,4 +1,4 @@ -import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; +import { SendEmailCommand } from "@aws-sdk/client-sesv2"; import { waitUntil } from "@vercel/functions"; import { Autumn as autumn } from "autumn-js"; import { and, eq } from "drizzle-orm"; @@ -8,6 +8,7 @@ import { getTenantSendingInfoForDomainOrParent, type TenantSendingInfo, } from "@/lib/aws-ses/identity-arn-helper"; +import { getSesClient } from "@/lib/aws-ses/ses-client"; import { db } from "@/lib/db"; import { SENT_EMAIL_STATUS, @@ -32,22 +33,7 @@ import { } from "../helper/attachment-processor"; import { validateAndRateLimit } from "../lib/auth"; -// Initialize SES client -const awsRegion = process.env.AWS_REGION || "us-east-2"; -const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; -const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; - -let sesClient: SESv2Client | null = null; - -if (awsAccessKeyId && awsSecretAccessKey) { - sesClient = new SESv2Client({ - region: awsRegion, - credentials: { - accessKeyId: awsAccessKeyId, - secretAccessKey: awsSecretAccessKey, - }, - }); -} +const sesClient = getSesClient(); // Request schema const AttachmentSchema = t.Object({ diff --git a/app/api/e2/emails/send.ts b/app/api/e2/emails/send.ts index 8548fe40..dca91d3e 100644 --- a/app/api/e2/emails/send.ts +++ b/app/api/e2/emails/send.ts @@ -1,4 +1,4 @@ -import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; +import { SendEmailCommand } from "@aws-sdk/client-sesv2"; import { Client as QStashClient } from "@upstash/qstash"; import { waitUntil } from "@vercel/functions"; import { Autumn as autumn } from "autumn-js"; @@ -10,6 +10,7 @@ import { getTenantSendingInfoForDomainOrParent, type TenantSendingInfo, } from "@/lib/aws-ses/identity-arn-helper"; +import { getSesClient } from "@/lib/aws-ses/ses-client"; import { db } from "@/lib/db"; import { SCHEDULED_EMAIL_STATUS, @@ -39,22 +40,7 @@ import { import { buildRawEmailMessage } from "../helper/email-builder"; import { validateAndRateLimit } from "../lib/auth"; -// Initialize SES client -const awsRegion = process.env.AWS_REGION || "us-east-2"; -const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; -const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; - -let sesClient: SESv2Client | null = null; - -if (awsAccessKeyId && awsSecretAccessKey) { - sesClient = new SESv2Client({ - region: awsRegion, - credentials: { - accessKeyId: awsAccessKeyId, - secretAccessKey: awsSecretAccessKey, - }, - }); -} +const sesClient = getSesClient(); // Request schema const AttachmentSchema = t.Object({ diff --git a/app/api/e2/lib/auth.ts b/app/api/e2/lib/auth.ts index 51c79434..d1062808 100644 --- a/app/api/e2/lib/auth.ts +++ b/app/api/e2/lib/auth.ts @@ -137,9 +137,9 @@ export async function validateAndRateLimit( } else if ( apiSession?.valid && !apiSession?.error && - apiSession?.key?.userId + apiSession?.key?.referenceId ) { - userId = apiSession.key.userId; + userId = apiSession.key.referenceId; console.log("🔑 [E2] Auth Type: API_KEY"); console.log("🔑 [E2] API Key:", apiKey); console.log("✅ API key authentication successful for userId:", userId); diff --git a/app/api/webhooks/send-email/build-ses-command.ts b/app/api/webhooks/send-email/build-ses-command.ts new file mode 100644 index 00000000..2f86da18 --- /dev/null +++ b/app/api/webhooks/send-email/build-ses-command.ts @@ -0,0 +1,45 @@ +import { SendEmailCommand } from "@aws-sdk/client-sesv2"; +import type { TenantSendingInfo } from "@/lib/aws-ses/identity-arn-helper"; +import { extractEmailAddress } from "@/lib/utils/email-utils"; + +/** + * Build a SendEmailCommand for SES with tenant-level tracking. + * Shared between handleScheduledEmail and handleBatchEmail. + */ +export function buildSesCommand(params: { + fromAddress: string; + toAddresses: string[]; + ccAddresses: string[]; + bccAddresses: string[]; + rawMessage: string; + tenantSendingInfo: TenantSendingInfo; +}): SendEmailCommand { + return new SendEmailCommand({ + FromEmailAddress: params.fromAddress, + ...(params.tenantSendingInfo.identityArn && { + FromEmailAddressIdentityArn: params.tenantSendingInfo.identityArn, + }), + Destination: { + ToAddresses: params.toAddresses.map(extractEmailAddress), + CcAddresses: + params.ccAddresses.length > 0 + ? params.ccAddresses.map(extractEmailAddress) + : undefined, + BccAddresses: + params.bccAddresses.length > 0 + ? params.bccAddresses.map(extractEmailAddress) + : undefined, + }, + Content: { + Raw: { + Data: Buffer.from(params.rawMessage), + }, + }, + ...(params.tenantSendingInfo.configurationSetName && { + ConfigurationSetName: params.tenantSendingInfo.configurationSetName, + }), + ...(params.tenantSendingInfo.tenantName && { + TenantName: params.tenantSendingInfo.tenantName, + }), + }); +} diff --git a/app/api/webhooks/send-email/resolve-tenant-info.ts b/app/api/webhooks/send-email/resolve-tenant-info.ts new file mode 100644 index 00000000..81c31ad5 --- /dev/null +++ b/app/api/webhooks/send-email/resolve-tenant-info.ts @@ -0,0 +1,73 @@ +import { + getAgentIdentityArn, + getTenantSendingInfoForDomainOrParent, + type TenantSendingInfo, +} from "@/lib/aws-ses/identity-arn-helper"; +import { getRootDomain, isSubdomain } from "@/lib/domains-and-dns/domain-utils"; + +/** + * Resolve tenant sending info (identity ARN, configuration set, tenant name) + * for a given user/domain/agent combination. Shared between handleScheduledEmail + * and handleBatchEmail. + */ +export async function resolveTenantInfo( + userId: string, + fromDomain: string, + isAgentEmail: boolean, + label: string, +): Promise { + let tenantSendingInfo: TenantSendingInfo = { + identityArn: null, + configurationSetName: null, + tenantName: null, + }; + + if (isAgentEmail) { + tenantSendingInfo = { + identityArn: getAgentIdentityArn(), + configurationSetName: null, + tenantName: null, + }; + } else { + const parentDomain = isSubdomain(fromDomain) + ? getRootDomain(fromDomain) + : undefined; + tenantSendingInfo = await getTenantSendingInfoForDomainOrParent( + userId, + fromDomain, + parentDomain || undefined, + ); + } + + if (tenantSendingInfo.identityArn) { + console.log( + `🏢 Using SourceArn for ${label} tenant tracking: ${tenantSendingInfo.identityArn}`, + ); + } else { + console.warn( + `⚠️ No SourceArn available - ${label} will not be tracked at tenant level`, + ); + } + + if (tenantSendingInfo.configurationSetName) { + console.log( + `📋 Using ConfigurationSet for ${label} tenant tracking: ${tenantSendingInfo.configurationSetName}`, + ); + } else { + console.warn( + `⚠️ No ConfigurationSet available - ${label} metrics may not be tracked correctly`, + ); + } + + if (tenantSendingInfo.tenantName) { + console.log( + `🏠 Using TenantName for ${label} AWS SES tracking: ${tenantSendingInfo.tenantName}`, + ); + } else { + console.warn( + `⚠️ No TenantName available - ${label} will NOT appear in tenant dashboard!`, + ); + } + + return tenantSendingInfo; +} diff --git a/app/api/webhooks/send-email/route.ts b/app/api/webhooks/send-email/route.ts index bf72e827..6d89741a 100644 --- a/app/api/webhooks/send-email/route.ts +++ b/app/api/webhooks/send-email/route.ts @@ -1,15 +1,10 @@ -import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; import { Receiver } from "@upstash/qstash"; import { waitUntil } from "@vercel/functions"; import { eq } from "drizzle-orm"; import { nanoid } from "nanoid"; import { type NextRequest, NextResponse } from "next/server"; import type { PostEmailsRequest } from "@/lib/api-types"; -import { - getAgentIdentityArn, - getTenantSendingInfoForDomainOrParent, - type TenantSendingInfo, -} from "@/lib/aws-ses/identity-arn-helper"; +import { getSesClient } from "@/lib/aws-ses/ses-client"; import { db } from "@/lib/db"; import { SCHEDULED_EMAIL_STATUS, @@ -17,7 +12,6 @@ import { scheduledEmails, sentEmails, } from "@/lib/db/schema"; -import { getRootDomain, isSubdomain } from "@/lib/domains-and-dns/domain-utils"; import { canUserSendFromEmail, extractEmailAddress, @@ -25,7 +19,10 @@ import { import { evaluateSending } from "@/lib/email-management/email-evaluation"; import { enforceOutboundSendGuard } from "@/lib/email-management/outbound-send-guard"; import { checkSendingSpike } from "@/lib/email-management/sending-spike-detector"; +import { normalizeAttachments } from "@/lib/utils/attachment-utils"; import { buildRawEmailMessage } from "../../e2/helper/email-builder"; +import { buildSesCommand } from "./build-ses-command"; +import { resolveTenantInfo } from "./resolve-tenant-info"; /** * POST /api/webhooks/send-email @@ -39,26 +36,7 @@ import { buildRawEmailMessage } from "../../e2/helper/email-builder"; * Has types? ✅ */ -// Initialize SES client -const awsRegion = process.env.AWS_REGION || "us-east-2"; -const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; -const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; - -let sesClient: SESv2Client | null = null; - -if (awsAccessKeyId && awsSecretAccessKey) { - sesClient = new SESv2Client({ - region: awsRegion, - credentials: { - accessKeyId: awsAccessKeyId, - secretAccessKey: awsSecretAccessKey, - }, - }); -} else { - console.warn( - "⚠️ AWS credentials not configured. Scheduled email processing will not work.", - ); -} +const sesClient = getSesClient(); // Initialize QStash receiver for signature verification const qstashReceiver = new Receiver({ @@ -76,13 +54,6 @@ interface QStashPayload { batchIndex?: number; // for batch } -interface StoredAttachment { - filename?: string; - contentType?: string; - content_type?: string; - [key: string]: unknown; -} - export async function POST(request: NextRequest) { console.log("📨 QStash Webhook - Received scheduled email request"); @@ -257,71 +228,7 @@ async function handleScheduledEmail(payload: QStashPayload) { : []; // Validate and fix attachment data - ensure contentType is set - const attachments = rawAttachments.map( - (att: StoredAttachment, index: number) => { - if (!att.contentType && !att.content_type) { - console.log( - `⚠️ Attachment ${index + 1} missing contentType, using fallback`, - ); - const filename = att.filename || "unknown"; - const ext = filename.toLowerCase().split(".").pop(); - let contentType = "application/octet-stream"; - - // Common file type mappings - switch (ext) { - case "pdf": - contentType = "application/pdf"; - break; - case "jpg": - case "jpeg": - contentType = "image/jpeg"; - break; - case "png": - contentType = "image/png"; - break; - case "gif": - contentType = "image/gif"; - break; - case "txt": - contentType = "text/plain"; - break; - case "html": - contentType = "text/html"; - break; - case "json": - contentType = "application/json"; - break; - case "zip": - contentType = "application/zip"; - break; - case "doc": - contentType = "application/msword"; - break; - case "docx": - contentType = - "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - break; - case "xls": - contentType = "application/vnd.ms-excel"; - break; - case "xlsx": - contentType = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - break; - } - - return { - ...att, - contentType: contentType, - }; - } - - return { - ...att, - contentType: att.contentType || att.content_type, - }; - }, - ); + const attachments = normalizeAttachments(rawAttachments); // Create sent email record first (for tracking) const sentEmailId = nanoid(); @@ -365,95 +272,25 @@ async function handleScheduledEmail(payload: QStashPayload) { textBody: scheduledEmail.textBody || undefined, htmlBody: scheduledEmail.htmlBody || undefined, customHeaders: headers, - attachments: attachments, + attachments, date: new Date(), }); - // Get the tenant sending info (identity ARN, configuration set, and tenant name) for tenant-level tracking - const fromDomain = scheduledEmail.fromDomain; - - let tenantSendingInfo: TenantSendingInfo = { - identityArn: null, - configurationSetName: null, - tenantName: null, - }; - if (isAgentEmail) { - tenantSendingInfo = { - identityArn: getAgentIdentityArn(), - configurationSetName: null, - tenantName: null, - }; - } else { - const parentDomain = isSubdomain(fromDomain) - ? getRootDomain(fromDomain) - : undefined; - tenantSendingInfo = await getTenantSendingInfoForDomainOrParent( - scheduledEmail.userId, - fromDomain, - parentDomain || undefined, - ); - } - - if (tenantSendingInfo.identityArn) { - console.log( - `🏢 Using SourceArn for scheduled email tenant tracking: ${tenantSendingInfo.identityArn}`, - ); - } else { - console.warn( - "⚠️ No SourceArn available - scheduled email will not be tracked at tenant level", - ); - } - - if (tenantSendingInfo.configurationSetName) { - console.log( - `📋 Using ConfigurationSet for scheduled email tenant tracking: ${tenantSendingInfo.configurationSetName}`, - ); - } else { - console.warn( - "⚠️ No ConfigurationSet available - scheduled email metrics may not be tracked correctly", - ); - } - - if (tenantSendingInfo.tenantName) { - console.log( - `🏠 Using TenantName for scheduled email AWS SES tracking: ${tenantSendingInfo.tenantName}`, - ); - } else { - console.warn( - "⚠️ No TenantName available - scheduled email will NOT appear in tenant dashboard!", - ); - } + // Resolve tenant sending info and build SES command + const tenantSendingInfo = await resolveTenantInfo( + scheduledEmail.userId, + scheduledEmail.fromDomain, + isAgentEmail, + "scheduled email", + ); - // Send via AWS SES using SESv2 SendEmailCommand with TenantName - // Per AWS docs: https://docs.aws.amazon.com/ses/latest/dg/tenants.html - // Use full fromAddress (with display name) for proper sender name display - const rawCommand = new SendEmailCommand({ - FromEmailAddress: scheduledEmail.fromAddress, - ...(tenantSendingInfo.identityArn && { - FromEmailAddressIdentityArn: tenantSendingInfo.identityArn, - }), - Destination: { - ToAddresses: toAddresses.map(extractEmailAddress), - CcAddresses: - ccAddresses.length > 0 - ? ccAddresses.map(extractEmailAddress) - : undefined, - BccAddresses: - bccAddresses.length > 0 - ? bccAddresses.map(extractEmailAddress) - : undefined, - }, - Content: { - Raw: { - Data: Buffer.from(rawMessage), - }, - }, - ...(tenantSendingInfo.configurationSetName && { - ConfigurationSetName: tenantSendingInfo.configurationSetName, - }), - ...(tenantSendingInfo.tenantName && { - TenantName: tenantSendingInfo.tenantName, - }), + const rawCommand = buildSesCommand({ + fromAddress: scheduledEmail.fromAddress, + toAddresses, + ccAddresses, + bccAddresses, + rawMessage, + tenantSendingInfo, }); const sesResponse = await sesClient.send(rawCommand); @@ -666,71 +503,7 @@ async function handleBatchEmail( : []; // Validate and fix attachment data - ensure contentType is set - const attachments = rawAttachments.map( - (att: StoredAttachment, index: number) => { - if (!att.contentType && !att.content_type) { - console.log( - `⚠️ Attachment ${index + 1} missing contentType, using fallback`, - ); - const filename = att.filename || "unknown"; - const ext = filename.toLowerCase().split(".").pop(); - let contentType = "application/octet-stream"; - - // Common file type mappings - switch (ext) { - case "pdf": - contentType = "application/pdf"; - break; - case "jpg": - case "jpeg": - contentType = "image/jpeg"; - break; - case "png": - contentType = "image/png"; - break; - case "gif": - contentType = "image/gif"; - break; - case "txt": - contentType = "text/plain"; - break; - case "html": - contentType = "text/html"; - break; - case "json": - contentType = "application/json"; - break; - case "zip": - contentType = "application/zip"; - break; - case "doc": - contentType = "application/msword"; - break; - case "docx": - contentType = - "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - break; - case "xls": - contentType = "application/vnd.ms-excel"; - break; - case "xlsx": - contentType = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - break; - } - - return { - ...att, - contentType: contentType, - }; - } - - return { - ...att, - contentType: att.contentType || att.content_type, - }; - }, - ); + const attachments = normalizeAttachments(rawAttachments); // Build raw email message console.log("📧 Building raw email message for batch email"); @@ -744,95 +517,25 @@ async function handleBatchEmail( textBody: sentEmail.textBody || undefined, htmlBody: sentEmail.htmlBody || undefined, customHeaders: headers, - attachments: attachments, + attachments, date: new Date(), }); - // Get the tenant sending info (identity ARN, configuration set, and tenant name) for tenant-level tracking - const batchFromDomain = sentEmail.fromDomain; - - let batchTenantInfo: TenantSendingInfo = { - identityArn: null, - configurationSetName: null, - tenantName: null, - }; - if (batchIsAgentEmail) { - batchTenantInfo = { - identityArn: getAgentIdentityArn(), - configurationSetName: null, - tenantName: null, - }; - } else { - const batchParentDomain = isSubdomain(batchFromDomain) - ? getRootDomain(batchFromDomain) - : undefined; - batchTenantInfo = await getTenantSendingInfoForDomainOrParent( - effectiveUserId, - batchFromDomain, - batchParentDomain || undefined, - ); - } - - if (batchTenantInfo.identityArn) { - console.log( - `🏢 Using SourceArn for batch email tenant tracking: ${batchTenantInfo.identityArn}`, - ); - } else { - console.warn( - "⚠️ No SourceArn available - batch email will not be tracked at tenant level", - ); - } - - if (batchTenantInfo.configurationSetName) { - console.log( - `📋 Using ConfigurationSet for batch email tenant tracking: ${batchTenantInfo.configurationSetName}`, - ); - } else { - console.warn( - "⚠️ No ConfigurationSet available - batch email metrics may not be tracked correctly", - ); - } - - if (batchTenantInfo.tenantName) { - console.log( - `🏠 Using TenantName for batch email AWS SES tracking: ${batchTenantInfo.tenantName}`, - ); - } else { - console.warn( - "⚠️ No TenantName available - batch email will NOT appear in tenant dashboard!", - ); - } + // Resolve tenant sending info and build SES command + const batchTenantInfo = await resolveTenantInfo( + effectiveUserId, + sentEmail.fromDomain, + batchIsAgentEmail, + "batch email", + ); - // Send via AWS SES using SESv2 SendEmailCommand with TenantName - // Per AWS docs: https://docs.aws.amazon.com/ses/latest/dg/tenants.html - // Use sentEmail.from (with display name) for proper sender name display - const rawCommand = new SendEmailCommand({ - FromEmailAddress: sentEmail.from, - ...(batchTenantInfo.identityArn && { - FromEmailAddressIdentityArn: batchTenantInfo.identityArn, - }), - Destination: { - ToAddresses: toAddresses.map(extractEmailAddress), - CcAddresses: - ccAddresses.length > 0 - ? ccAddresses.map(extractEmailAddress) - : undefined, - BccAddresses: - bccAddresses.length > 0 - ? bccAddresses.map(extractEmailAddress) - : undefined, - }, - Content: { - Raw: { - Data: Buffer.from(rawMessage), - }, - }, - ...(batchTenantInfo.configurationSetName && { - ConfigurationSetName: batchTenantInfo.configurationSetName, - }), - ...(batchTenantInfo.tenantName && { - TenantName: batchTenantInfo.tenantName, - }), + const rawCommand = buildSesCommand({ + fromAddress: sentEmail.from, + toAddresses, + ccAddresses, + bccAddresses, + rawMessage, + tenantSendingInfo: batchTenantInfo, }); const sesResponse = await sesClient.send(rawCommand); diff --git a/bun.lockb b/bun.lockb index 561946345afc7b2270d3eaabf6034999db4fc079..9c762caba621e42d3183706318b85766a8fe2f37 100755 GIT binary patch delta 145426 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%0A^OVc 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%0DW6X5cDD< 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%0AHCJ7ZMixf}?BXOBAjC(tndemR#9fbqo3p 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 Y`MBEz=sGs)^3}j*{Th+i) { const queryClient = useQueryClient(); @@ -98,11 +101,11 @@ export const useRevokeAllApiKeysMutation = () => { throw new Error(listError.message); } - if (!apiKeys || apiKeys.length === 0) { + if (!apiKeys?.apiKeys || apiKeys.apiKeys.length === 0) { return { count: 0 }; } - const enabledKeys = apiKeys.filter((key) => key.enabled); + const enabledKeys = apiKeys.apiKeys.filter((key) => key.enabled); if (enabledKeys.length === 0) { return { count: 0 }; } diff --git a/features/settings/hooks/useApiKeysQuery.ts b/features/settings/hooks/useApiKeysQuery.ts index 5b5e292a..fd1319f6 100644 --- a/features/settings/hooks/useApiKeysQuery.ts +++ b/features/settings/hooks/useApiKeysQuery.ts @@ -1,17 +1,17 @@ -import { useQuery } from '@tanstack/react-query' -import { authClient } from '@/lib/auth/auth-client' +import { useQuery } from "@tanstack/react-query"; +import { authClient } from "@/lib/auth/auth-client"; export const useApiKeysQuery = () => { - return useQuery({ - queryKey: ['apiKeys'], - queryFn: async () => { - const { data, error } = await authClient.apiKey.list() - if (error) { - throw new Error(error.message) - } - return data || [] - }, - staleTime: 3 * 60 * 1000, // 3 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - }) -} \ No newline at end of file + return useQuery({ + queryKey: ["apiKeys"], + queryFn: async () => { + const { data, error } = await authClient.apiKey.list(); + if (error) { + throw new Error(error.message); + } + return data?.apiKeys || []; + }, + staleTime: 3 * 60 * 1000, // 3 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }); +}; diff --git a/lib/auth/auth-client.ts b/lib/auth/auth-client.ts index 6eb529b9..577a2d0b 100644 --- a/lib/auth/auth-client.ts +++ b/lib/auth/auth-client.ts @@ -1,9 +1,7 @@ +import { apiKeyClient } from "@better-auth/api-key/client"; +import { sentinelClient } from "@better-auth/infra/client"; import { passkeyClient } from "@better-auth/passkey/client"; -import { - adminClient, - apiKeyClient, - magicLinkClient, -} from "better-auth/client/plugins"; +import { adminClient, magicLinkClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ @@ -13,7 +11,13 @@ export const authClient = createAuthClient({ : process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://inbound.new", - plugins: [adminClient(), apiKeyClient(), magicLinkClient(), passkeyClient()], + plugins: [ + adminClient(), + apiKeyClient(), + magicLinkClient(), + passkeyClient(), + sentinelClient(), + ], }); export const { signIn, signUp, signOut, useSession } = authClient; diff --git a/lib/auth/auth.ts b/lib/auth/auth.ts index d0c01fc5..f41a412f 100644 --- a/lib/auth/auth.ts +++ b/lib/auth/auth.ts @@ -1,13 +1,16 @@ +import { apiKey } from "@better-auth/api-key"; +import { dash } from "@better-auth/infra"; import { passkey } from "@better-auth/passkey"; import { render } from "@react-email/components"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { createAuthMiddleware } from "better-auth/api"; -import { admin, apiKey, magicLink, oAuthProxy } from "better-auth/plugins"; +import { admin, magicLink, oAuthProxy } from "better-auth/plugins"; import { and, eq } from "drizzle-orm"; import Inbound from "inboundemail"; import MagicLinkEmail from "@/emails/magic-link-email"; +import { extractDomainFromEmail } from "@/lib/utils/email-utils"; import { db } from "../db/index"; import * as schema from "../db/schema"; @@ -73,7 +76,7 @@ const inbound = new Inbound({ * Check if an email domain is blocked from signing up */ async function isBlockedEmailDomain(email: string): Promise { - const domain = email.split("@")[1]?.toLowerCase(); + const domain = extractDomainFromEmail(email); if (!domain) return false; if (BLOCKED_SIGNUP_DOMAINS.includes(domain)) { @@ -203,6 +206,7 @@ export const auth = betterAuth({ } }, }), + dash(), ], hooks: { before: createAuthMiddleware(async (ctx) => { diff --git a/lib/auth/v2-auth.ts b/lib/auth/v2-auth.ts index f1a50a99..3dcf08b5 100644 --- a/lib/auth/v2-auth.ts +++ b/lib/auth/v2-auth.ts @@ -1,201 +1,207 @@ /** * V2 API Authentication Utility - * + * * Provides unified authentication for v2 API routes supporting both: * 1. Session-based authentication (for web app users) * 2. API key authentication (for programmatic access) - * + * * Following the API management rules, this utility checks session first, * then falls back to API key authentication if no session is found. */ -import { NextRequest } from 'next/server' -import { auth } from '@/lib/auth/auth' -import { headers } from 'next/headers' +import { headers } from "next/headers"; +import type { NextRequest } from "next/server"; +import { auth } from "@/lib/auth/auth"; export interface AuthenticationResult { - success: boolean - user?: { - id: string - email: string - name: string | null - } - authType?: 'session' | 'api_key' - error?: string + success: boolean; + user?: { + id: string; + email: string; + name: string | null; + }; + authType?: "session" | "api_key"; + error?: string; } /** * Unified authentication for v2 API routes * Checks session first, then API key if no session found */ -export async function authenticateV2Request(request: NextRequest): Promise { - try { - // Step 1: Try session-based authentication first - const sessionResult = await trySessionAuth() - if (sessionResult.success) { - return { - ...sessionResult, - authType: 'session' - } - } - - // Step 2: If no session, try API key authentication - const apiKeyResult = await tryApiKeyAuth(request) - if (apiKeyResult.success) { - return { - ...apiKeyResult, - authType: 'api_key' - } - } - - // Step 3: Both authentication methods failed - return { - success: false, - error: 'Authentication required. Please provide a valid session or API key.' - } - } catch (error) { - console.error('V2 API authentication error:', error) - return { - success: false, - error: 'Internal authentication error' - } - } +export async function authenticateV2Request( + request: NextRequest, +): Promise { + try { + // Step 1: Try session-based authentication first + const sessionResult = await trySessionAuth(); + if (sessionResult.success) { + return { + ...sessionResult, + authType: "session", + }; + } + + // Step 2: If no session, try API key authentication + const apiKeyResult = await tryApiKeyAuth(request); + if (apiKeyResult.success) { + return { + ...apiKeyResult, + authType: "api_key", + }; + } + + // Step 3: Both authentication methods failed + return { + success: false, + error: + "Authentication required. Please provide a valid session or API key.", + }; + } catch (error) { + console.error("V2 API authentication error:", error); + return { + success: false, + error: "Internal authentication error", + }; + } } /** * Try session-based authentication */ async function trySessionAuth(): Promise { - try { - const session = await auth.api.getSession({ - headers: await headers() - }) - - if (!session?.user?.id) { - return { - success: false, - error: 'No valid session found' - } - } - - return { - success: true, - user: { - id: session.user.id, - email: session.user.email || '', - name: session.user.name || null - } - } - } catch (error) { - return { - success: false, - error: 'Session validation failed' - } - } + try { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user?.id) { + return { + success: false, + error: "No valid session found", + }; + } + + return { + success: true, + user: { + id: session.user.id, + email: session.user.email || "", + name: session.user.name || null, + }, + }; + } catch (error) { + return { + success: false, + error: "Session validation failed", + }; + } } /** * Try API key authentication */ -async function tryApiKeyAuth(request: NextRequest): Promise { - try { - // Get the Authorization header - const authHeader = request.headers.get('Authorization') - - if (!authHeader) { - return { - success: false, - error: 'No Authorization header found' - } - } - - // Extract the API key (support both "Bearer " and just "") - let apiKey: string - if (authHeader.startsWith('Bearer ')) { - apiKey = authHeader.substring(7) - } else { - apiKey = authHeader - } - - if (!apiKey) { - return { - success: false, - error: 'Invalid Authorization header format' - } - } - - // Verify the API key using Better Auth - const { valid, error, key } = await auth.api.verifyApiKey({ - body: { - key: apiKey - } - }) - - if (!valid || error || !key) { - return { - success: false, - error: error?.message || 'Invalid API key' - } - } - - // Check if the API key is enabled - if (!key.enabled) { - return { - success: false, - error: 'API key is disabled' - } - } - - // Check if the API key has expired - if (key.expiresAt && new Date(key.expiresAt) < new Date()) { - return { - success: false, - error: 'API key has expired' - } - } - - return { - success: true, - user: { - id: key.userId, - email: key.userId, // We don't have email from API key, use userId as fallback - name: null // We don't have name from API key - } - } - } catch (error) { - console.error('API key authentication error:', error) - return { - success: false, - error: 'API key validation failed' - } - } +async function tryApiKeyAuth( + request: NextRequest, +): Promise { + try { + // Get the Authorization header + const authHeader = request.headers.get("Authorization"); + + if (!authHeader) { + return { + success: false, + error: "No Authorization header found", + }; + } + + // Extract the API key (support both "Bearer " and just "") + let apiKey: string; + if (authHeader.startsWith("Bearer ")) { + apiKey = authHeader.substring(7); + } else { + apiKey = authHeader; + } + + if (!apiKey) { + return { + success: false, + error: "Invalid Authorization header format", + }; + } + + // Verify the API key using Better Auth + const { valid, error, key } = await auth.api.verifyApiKey({ + body: { + key: apiKey, + }, + }); + + if (!valid || error || !key) { + return { + success: false, + error: (error?.message as string) || "Invalid API key", + }; + } + + // Check if the API key is enabled + if (!key.enabled) { + return { + success: false, + error: "API key is disabled", + }; + } + + // Check if the API key has expired + if (key.expiresAt && new Date(key.expiresAt) < new Date()) { + return { + success: false, + error: "API key has expired", + }; + } + + return { + success: true, + user: { + id: key.referenceId, + email: key.referenceId, // We don't have email from API key, use referenceId as fallback + name: null, // We don't have name from API key + }, + }; + } catch (error) { + console.error("API key authentication error:", error); + return { + success: false, + error: "API key validation failed", + }; + } } /** * Get authentication error response with proper status code */ export function getAuthErrorResponse(authResult: AuthenticationResult): { - error: string - details?: string - status: number + error: string; + details?: string; + status: number; } { - if (authResult.error?.includes('expired')) { - return { - error: authResult.error, - status: 401 - } - } - - if (authResult.error?.includes('disabled')) { - return { - error: authResult.error, - status: 403 - } - } - - return { - error: authResult.error || 'Authentication failed', - details: 'Please provide a valid session or API key in the Authorization header', - status: 401 - } -} \ No newline at end of file + if (authResult.error?.includes("expired")) { + return { + error: authResult.error, + status: 401, + }; + } + + if (authResult.error?.includes("disabled")) { + return { + error: authResult.error, + status: 403, + }; + } + + return { + error: authResult.error || "Authentication failed", + details: + "Please provide a valid session or API key in the Authorization header", + status: 401, + }; +} diff --git a/lib/aws-ses/ses-client.ts b/lib/aws-ses/ses-client.ts new file mode 100644 index 00000000..d326a308 --- /dev/null +++ b/lib/aws-ses/ses-client.ts @@ -0,0 +1,48 @@ +/** + * Shared SES v2 client factory + * + * Provides a cached SESv2Client instance so every module that sends email + * doesn't repeat the credential-loading boilerplate. + */ + +import { SESv2Client } from "@aws-sdk/client-sesv2"; + +const awsRegion = process.env.AWS_REGION || "us-east-2"; +const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; +const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; + +let cachedClient: SESv2Client | null = null; + +/** + * Returns the shared SESv2Client, or `null` if credentials are missing. + */ +export function getSesClient(): SESv2Client | null { + if (cachedClient) return cachedClient; + + if (!awsAccessKeyId || !awsSecretAccessKey) { + return null; + } + + cachedClient = new SESv2Client({ + region: awsRegion, + credentials: { + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, + }, + }); + + return cachedClient; +} + +/** + * Returns the shared SESv2Client, throwing if credentials are missing. + */ +export function requireSesClient(): SESv2Client { + const client = getSesClient(); + if (!client) { + throw new Error( + "AWS SES credentials not configured (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY missing)", + ); + } + return client; +} diff --git a/lib/email-management/agent-email-helper.test.ts b/lib/email-management/agent-email-helper.test.ts new file mode 100644 index 00000000..62ffc6d4 --- /dev/null +++ b/lib/email-management/agent-email-helper.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "bun:test"; +import { + isAgentEmail, + canUserSendFromEmail, +} from "@/lib/email-management/agent-email-helper"; + +describe("isAgentEmail", () => { + it("returns true for exact agent@inbnd.dev", () => { + expect(isAgentEmail("agent@inbnd.dev")).toBe(true); + }); + + it("is case insensitive", () => { + expect(isAgentEmail("Agent@INBND.DEV")).toBe(true); + }); + + it("handles Name format", () => { + expect(isAgentEmail("Inbound Agent ")).toBe(true); + }); + + it("returns false for non-agent address", () => { + expect(isAgentEmail("user@example.com")).toBe(false); + }); + + it("returns false for similar but different address", () => { + expect(isAgentEmail("agent@inbnd.com")).toBe(false); + }); +}); + +describe("canUserSendFromEmail", () => { + it("identifies agent email", () => { + const result = canUserSendFromEmail("agent@inbnd.dev"); + expect(result.isAgentEmail).toBe(true); + expect(result.domain).toBe("inbnd.dev"); + }); + + it("identifies custom domain", () => { + const result = canUserSendFromEmail("user@custom.com"); + expect(result.isAgentEmail).toBe(false); + expect(result.domain).toBe("custom.com"); + }); + + it("handles Name format", () => { + const result = canUserSendFromEmail("My Name "); + expect(result.isAgentEmail).toBe(false); + expect(result.domain).toBe("mydomain.io"); + }); +}); diff --git a/lib/email-management/agent-email-helper.ts b/lib/email-management/agent-email-helper.ts index d3bc0a81..2fc8250a 100644 --- a/lib/email-management/agent-email-helper.ts +++ b/lib/email-management/agent-email-helper.ts @@ -3,55 +3,18 @@ * This email can be used by any user for sending emails through the v2 APIs */ -/** - * Check if an email address is the special agent@inbnd.dev address - */ -export function isAgentEmail(email: string): boolean { - // Extract just the email address part, removing any name formatting - const emailMatch = email.match(/<([^>]+)>/) || [null, email] - const cleanEmail = emailMatch[1] || email - - return cleanEmail.toLowerCase() === 'agent@inbnd.dev' -} +import { extractDomainFromEmail, extractEmailAddress } from "@/lib/utils/email-utils"; -/** - * Extract domain from email address - */ -export function extractDomain(email: string): string { - // Extract just the email address part, removing any name formatting - const emailMatch = email.match(/<([^>]+)>/) || [null, email] - const cleanEmail = emailMatch[1] || email - - const parts = cleanEmail.split('@') - return parts.length === 2 ? parts[1].toLowerCase() : '' -} +// Re-export shared utilities so existing callers don't break +export { extractEmailAddress, extractEmailName } from "@/lib/utils/email-utils"; +export { extractDomainFromEmail as extractDomain } from "@/lib/utils/email-utils"; /** - * Extract email address from formatted email (removes name part) - */ -export function extractEmailAddress(email: string): string { - // Handle "Name " format - const emailMatch = email.match(/<([^>]+)>/) - if (emailMatch) { - return emailMatch[1] - } - - // Handle plain "email@domain.com" format - return email -} - -/** - * Extract name from formatted email (removes email part) + * Check if an email address is the special agent@inbnd.dev address */ -export function extractEmailName(email: string): string | null { - // Handle "Name " format - const nameMatch = email.match(/^(.+?)\s*<[^>]+>$/) - if (nameMatch) { - return nameMatch[1].trim().replace(/^["']|["']$/g, '') // Remove quotes if present - } - - // No name part found - return null +export function isAgentEmail(email: string): boolean { + const cleanEmail = extractEmailAddress(email); + return cleanEmail.toLowerCase() === "agent@inbnd.dev"; } /** @@ -60,12 +23,15 @@ export function extractEmailName(email: string): string | null { * 1. The email is agent@inbnd.dev (allowed for all users) * 2. The user owns the domain (checked separately in the API) */ -export function canUserSendFromEmail(email: string): { isAgentEmail: boolean; domain: string } { - const domain = extractDomain(email) - const isAgent = isAgentEmail(email) - - return { - isAgentEmail: isAgent, - domain - } +export function canUserSendFromEmail(email: string): { + isAgentEmail: boolean; + domain: string; +} { + const domain = extractDomainFromEmail(email); + const isAgent = isAgentEmail(email); + + return { + isAgentEmail: isAgent, + domain, + }; } diff --git a/lib/email-management/delivery-event-tracker.test.ts b/lib/email-management/delivery-event-tracker.test.ts new file mode 100644 index 00000000..1aaac36d --- /dev/null +++ b/lib/email-management/delivery-event-tracker.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "bun:test"; +import { getBounceSubType } from "@/lib/email-management/delivery-event-tracker"; + +describe("getBounceSubType", () => { + it("returns UNKNOWN when statusCode is undefined", () => { + expect(getBounceSubType(undefined, undefined)).toBe("unknown"); + }); + + it("maps 5.1.1 → user_unknown", () => { + expect(getBounceSubType("5.1.1", undefined)).toBe("user_unknown"); + }); + + it("maps 5.1.2 → bad_destination", () => { + expect(getBounceSubType("5.1.2", undefined)).toBe("bad_destination"); + }); + + it("maps 5.2.1 → mailbox_disabled", () => { + expect(getBounceSubType("5.2.1", undefined)).toBe("mailbox_disabled"); + }); + + it("maps 5.2.2 → mailbox_full", () => { + expect(getBounceSubType("5.2.2", undefined)).toBe("mailbox_full"); + }); + + it("maps 5.3.4 → message_too_large", () => { + expect(getBounceSubType("5.3.4", undefined)).toBe("message_too_large"); + }); + + it("maps 5.4.4 → invalid_domain", () => { + expect(getBounceSubType("5.4.4", undefined)).toBe("invalid_domain"); + }); + + it("maps 5.7.1 → policy_rejection", () => { + expect(getBounceSubType("5.7.1", undefined)).toBe("policy_rejection"); + }); + + it("maps 5.6.1 → content_rejected", () => { + expect(getBounceSubType("5.6.1", undefined)).toBe("content_rejected"); + }); + + it("maps 4.2.2 → mailbox_full", () => { + expect(getBounceSubType("4.2.2", undefined)).toBe("mailbox_full"); + }); + + it("maps 4.4.4 → dns_failure", () => { + expect(getBounceSubType("4.4.4", undefined)).toBe("dns_failure"); + }); + + it("maps 4.4.7 → delivery_timeout", () => { + expect(getBounceSubType("4.4.7", undefined)).toBe("delivery_timeout"); + }); + + it("maps 4.4.1 → connection_failed", () => { + expect(getBounceSubType("4.4.1", undefined)).toBe("connection_failed"); + }); + + it("overrides with suppression_list when diagnostic mentions it", () => { + expect( + getBounceSubType( + "5.1.1", + "550 Address is on the suppression list for this account", + ), + ).toBe("suppression_list"); + }); + + it("returns general_failure for unrecognized status codes", () => { + expect(getBounceSubType("5.9.9", undefined)).toBe("general_failure"); + }); +}); diff --git a/lib/email-management/delivery-event-tracker.ts b/lib/email-management/delivery-event-tracker.ts index b3bc3fdb..81c9c49b 100644 --- a/lib/email-management/delivery-event-tracker.ts +++ b/lib/email-management/delivery-event-tracker.ts @@ -22,6 +22,7 @@ import { emailDeliveryEvents, type NewEmailDeliveryEvent, } from "@/lib/db/schema"; +import { extractDomainFromEmail } from "@/lib/utils/email-utils"; import { dispatchEmailBouncedEvent } from "@/lib/svix/event-dispatcher"; import { getDsnSourceInfo, parseDsn } from "./dsn-parser"; @@ -58,7 +59,7 @@ export interface RecordDeliveryEventResult { /** * Determine the bounce sub-type from status code */ -function getBounceSubType( +export function getBounceSubType( statusCode: string | undefined, diagnosticCode: string | undefined, ): BounceSubType { @@ -90,15 +91,6 @@ function getBounceSubType( return statusMap[statusCode] || BOUNCE_SUB_TYPES.GENERAL_FAILURE; } -/** - * Extract domain from email address - */ -function extractDomain(email: string): string | undefined { - const atIndex = email.indexOf("@"); - if (atIndex === -1) return undefined; - return email.substring(atIndex + 1).toLowerCase(); -} - /** * Record a delivery event from a DSN */ @@ -145,7 +137,7 @@ export async function recordDeliveryEventFromDsn( } const eventId = `evt_${nanoid()}`; - const failedRecipientDomain = extractDomain(failedRecipient); + const failedRecipientDomain = extractDomainFromEmail(failedRecipient) || undefined; // Prepare the event record const eventRecord: NewEmailDeliveryEvent = { diff --git a/lib/email-management/dsn-parser.test.ts b/lib/email-management/dsn-parser.test.ts index f96c27ee..02beafee 100644 --- a/lib/email-management/dsn-parser.test.ts +++ b/lib/email-management/dsn-parser.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "bun:test"; -import { parseDsn } from "@/lib/email-management/dsn-parser"; +import { + isDsn, + parseDsn, + quickIsDsnCheck, +} from "@/lib/email-management/dsn-parser"; const SES_MESSAGE_ID = "010f019ae693fb1b-50675262-d740-487c-97b8-e6de49d2e104-000000"; @@ -50,4 +54,52 @@ describe("dsn-parser", () => { `${SES_MESSAGE_ID}@us-east-2.amazonses.com`, ); }); + + it("classifies 5.x.x as permanent failure (hard bounce)", async () => { + const dsn = await parseDsn(buildDsnRawContentWithLfOnly()); + expect(dsn.statusClass).toBe("5"); + expect(dsn.bounceType).toBe("hard"); + }); + + it("extracts diagnostic code", async () => { + const dsn = await parseDsn(buildDsnRawContentWithLfOnly()); + expect(dsn.deliveryStatus?.diagnosticCode).toContain("550 5.1.1"); + }); +}); + +describe("isDsn", () => { + it("returns true for content with delivery-status indicators", () => { + expect(isDsn(buildDsnRawContentWithLfOnly())).toBe(true); + }); + + it("returns true when headers indicate delivery-status", () => { + expect( + isDsn("plain body", { + "content-type": { + value: "multipart/report", + params: { "report-type": "delivery-status" }, + }, + }), + ).toBe(true); + }); + + it("returns false for regular email content", () => { + expect(isDsn("Hello, this is a normal email.")).toBe(false); + }); +}); + +describe("quickIsDsnCheck", () => { + it("extracts recipient and status from DSN content", () => { + const result = quickIsDsnCheck(buildDsnRawContentWithLfOnly()); + expect(result.isDsn).toBe(true); + expect(result.finalRecipient).toBe("bounce-target@example.com"); + expect(result.status).toBe("5.1.1"); + expect(result.diagnosticCode).toContain("550 5.1.1"); + }); + + it("returns isDsn false for non-DSN content", () => { + const result = quickIsDsnCheck("Just a normal email."); + expect(result.isDsn).toBe(false); + expect(result.finalRecipient).toBeUndefined(); + }); }); diff --git a/lib/email-management/email-blocking.ts b/lib/email-management/email-blocking.ts index 7c12b717..48eee4d9 100644 --- a/lib/email-management/email-blocking.ts +++ b/lib/email-management/email-blocking.ts @@ -2,13 +2,7 @@ import { db } from '@/lib/db' import { blockedEmails, emailDomains, emailAddresses } from '@/lib/db/schema' import { eq, and } from 'drizzle-orm' import { nanoid } from 'nanoid' - -/** - * Extract domain from email address - */ -function extractDomain(email: string): string { - return email.split('@')[1]?.toLowerCase() || '' -} +import { extractDomainFromEmail, extractEmailAddress, isValidEmail } from '@/lib/utils/email-utils' /** * Check if an email address is already blocked @@ -43,10 +37,7 @@ export async function checkRecipientsAgainstBlocklist( // Normalize all email addresses const normalizedRecipients = recipients.map(email => { - // Extract email from "Name " format if needed - const match = email.match(/<([^>]+)>/) - const extracted = match ? match[1] : email - return extracted.toLowerCase().trim() + return extractEmailAddress(email).toLowerCase().trim() }) // Query all blocked addresses at once @@ -82,8 +73,7 @@ export async function blockEmail( ): Promise<{ success: boolean; error?: string; message?: string }> { try { // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - if (!emailRegex.test(emailAddress)) { + if (!isValidEmail(emailAddress)) { return { success: false, error: 'Invalid email format' @@ -91,7 +81,7 @@ export async function blockEmail( } const normalizedEmail = emailAddress.toLowerCase() - const domain = extractDomain(normalizedEmail) + const domain = extractDomainFromEmail(normalizedEmail) if (!domain) { return { diff --git a/lib/email-management/email-forwarder.ts b/lib/email-management/email-forwarder.ts index 247e96f4..67c7ca31 100644 --- a/lib/email-management/email-forwarder.ts +++ b/lib/email-management/email-forwarder.ts @@ -1,4 +1,5 @@ -import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' +import { type SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' +import { requireSesClient } from '@/lib/aws-ses/ses-client' import type { ParsedEmailData } from './email-parser' import { generateEmailBannerHTML } from '@/components/email-banner' @@ -6,9 +7,7 @@ export class EmailForwarder { private sesClient: SESv2Client constructor() { - this.sesClient = new SESv2Client({ - region: process.env.AWS_REGION || 'us-east-2' - }) + this.sesClient = requireSesClient() } async forwardEmail( diff --git a/lib/email-management/email-parser.test.ts b/lib/email-management/email-parser.test.ts new file mode 100644 index 00000000..0506bf2e --- /dev/null +++ b/lib/email-management/email-parser.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "bun:test"; +import { + sanitizeHtml, + extractEmailDomain, + formatEmailAddress, + extractEmailAddress, + extractEmailAddresses, +} from "@/lib/email-management/email-parser"; + +describe("sanitizeHtml", () => { + it("removes script tags", () => { + const html = '

'; + const result = sanitizeHtml(html); + expect(result).not.toContain(" { + const html = '
Click
'; + const result = sanitizeHtml(html); + expect(result).not.toContain("onclick"); + }); + + it("removes single-quoted event handlers", () => { + const html = "
Hover
"; + const result = sanitizeHtml(html); + expect(result).not.toContain("onmouseover"); + }); + + it("removes javascript: URLs", () => { + const html = '
Click'; + const result = sanitizeHtml(html); + expect(result).not.toContain("javascript:"); + }); + + it("preserves data:image URLs", () => { + const html = + 'inline image'; + const result = sanitizeHtml(html); + expect(result).toContain("data:image/png"); + }); + + it("returns empty string for empty input", () => { + expect(sanitizeHtml("")).toBe(""); + }); +}); + +describe("extractEmailDomain", () => { + it("extracts domain from plain email", () => { + expect(extractEmailDomain("user@example.com")).toBe("example.com"); + }); + + it("extracts domain from angle-bracket email", () => { + expect(extractEmailDomain("")).toBe("example.com"); + }); + + it("returns empty string for no @ sign", () => { + expect(extractEmailDomain("nodomain")).toBe(""); + }); +}); + +describe("formatEmailAddress", () => { + it("parses Name format", () => { + const result = formatEmailAddress("John Doe "); + expect(result.name).toBe("John Doe"); + expect(result.address).toBe("john@example.com"); + }); + + it("parses quoted name format", () => { + const result = formatEmailAddress('"Jane Smith" '); + expect(result.name).toBe("Jane Smith"); + expect(result.address).toBe("jane@example.com"); + }); + + it("handles plain email", () => { + const result = formatEmailAddress("user@example.com"); + expect(result.name).toBe(""); + expect(result.address).toBe("user@example.com"); + }); +}); + +describe("extractEmailAddress (mailparser version)", () => { + it("returns unknown for null", () => { + expect(extractEmailAddress(null)).toBe("unknown"); + }); + + it("returns string input as-is", () => { + expect(extractEmailAddress("user@example.com")).toBe("user@example.com"); + }); + + it("extracts text from address object", () => { + expect(extractEmailAddress({ text: "John " })).toBe( + "John ", + ); + }); + + it("extracts from array of address objects", () => { + expect( + extractEmailAddress([{ text: "first@example.com" }]), + ).toBe("first@example.com"); + }); + + it("falls back to address field", () => { + expect(extractEmailAddress({ address: "addr@example.com" })).toBe( + "addr@example.com", + ); + }); + + it("falls back to name field", () => { + expect(extractEmailAddress({ name: "John" })).toBe("John"); + }); + + it("returns unknown for empty object", () => { + expect(extractEmailAddress({})).toBe("unknown"); + }); +}); + +describe("extractEmailAddresses", () => { + it("returns empty array for null", () => { + expect(extractEmailAddresses(null)).toEqual([]); + }); + + it("wraps string in array", () => { + expect(extractEmailAddresses("user@example.com")).toEqual([ + "user@example.com", + ]); + }); + + it("extracts from array of address objects", () => { + const result = extractEmailAddresses([ + { text: "a@example.com" }, + { address: "b@example.com" }, + ]); + expect(result).toEqual(["a@example.com", "b@example.com"]); + }); + + it("extracts from object with text field", () => { + expect(extractEmailAddresses({ text: "user@example.com" })).toEqual([ + "user@example.com", + ]); + }); +}); diff --git a/lib/email-management/email-router.ts b/lib/email-management/email-router.ts index 09b3530c..aeee31d3 100644 --- a/lib/email-management/email-router.ts +++ b/lib/email-management/email-router.ts @@ -23,12 +23,12 @@ import { evaluateGuardRules } from "../guard/rule-matcher"; import { checkRecipientsAgainstBlocklist } from "./email-blocking"; import { EmailForwarder } from "./email-forwarder"; import type { ParsedEmailData } from "./email-parser"; -import { sanitizeHtml } from "./email-parser"; import { EmailThreader, type ThreadingResult } from "./email-threader"; import { triggerEmailAction } from "./webhook-trigger"; - -// Maximum webhook payload size (5MB safety margin) -const MAX_WEBHOOK_PAYLOAD_SIZE = 1_000_000; +import { + constructWebhookPayload, + ensurePayloadSize, +} from "./webhook-payload"; /** * Main email routing function - routes emails to appropriate endpoints @@ -797,52 +797,13 @@ async function handleWebhookEndpoint( downloadUrl: `${baseUrl}/api/e2/attachments/${emailData.structuredId}/${encodeURIComponent(att.filename || "attachment")}`, })) || []; - // Create enhanced parsedData with download URLs - const enhancedParsedData = { - ...parsedEmailData, - attachments: attachmentsWithUrls, - }; - - // Create webhook payload with the exact structure expected - const webhookPayload = { - event: "email.received", - timestamp: new Date().toISOString(), - email: { - id: emailData.structuredId, // Use structured email ID for v2 API compatibility - messageId: emailData.messageId, - from: emailData.fromData ? JSON.parse(emailData.fromData) : null, - to: emailData.toData ? JSON.parse(emailData.toData) : null, - recipient: emailData.recipient, - subject: emailData.subject, - receivedAt: emailData.date, - - // Threading information - threadId: emailData.threadId || null, - threadPosition: emailData.threadPosition || null, - - // Full ParsedEmailData structure with download URLs - parsedData: enhancedParsedData, - - // Cleaned content for backward compatibility - cleanedContent: { - html: parsedEmailData.htmlBody - ? sanitizeHtml(parsedEmailData.htmlBody) - : null, - text: parsedEmailData.textBody || null, - hasHtml: !!parsedEmailData.htmlBody, - hasText: !!parsedEmailData.textBody, - attachments: attachmentsWithUrls, // Include download URLs in cleaned content too - headers: parsedEmailData.headers || {}, - }, - }, - endpoint: { - id: endpoint.id, - name: endpoint.name, - type: endpoint.type, - }, - }; - - const payloadString = JSON.stringify(webhookPayload); + // Build webhook payload + const webhookPayload = constructWebhookPayload( + emailData, + parsedEmailData, + attachmentsWithUrls, + endpoint, + ); // Prepare headers const headers: HeadersInit = { @@ -858,76 +819,8 @@ async function handleWebhookEndpoint( }; // Check payload size and strip fields if necessary - let finalPayload = webhookPayload; - let finalPayloadString = payloadString; - const strippedFields: string[] = []; - - if (payloadString.length > MAX_WEBHOOK_PAYLOAD_SIZE) { - console.warn( - `⚠️ handleWebhookEndpoint - Webhook payload too large (${payloadString.length} bytes), stripping attachment bodies from raw field`, - ); - - // Try stripping attachment bodies from raw field first - if (enhancedParsedData.raw) { - // Remove base64-encoded attachment bodies while preserving MIME structure and headers - // This regex finds ALL base64 content from header until next MIME boundary - const cleanedRaw = enhancedParsedData.raw.replace( - /Content-Transfer-Encoding:\s*base64\s*[\r\n]+[\r\n]+([\s\S]+?)(?=\r?\n--|\r?\n\r?\nContent-|$)/gi, - "Content-Transfer-Encoding: base64\r\n\r\n[binary attachment data removed - use Attachments API]\r\n", - ); - - const payloadWithCleanedRaw = { - ...webhookPayload, - email: { - ...webhookPayload.email, - parsedData: { - ...enhancedParsedData, - raw: cleanedRaw, - }, - }, - }; - const payloadStringWithCleanedRaw = JSON.stringify( - payloadWithCleanedRaw, - ); - - if (payloadStringWithCleanedRaw.length <= MAX_WEBHOOK_PAYLOAD_SIZE) { - finalPayload = payloadWithCleanedRaw; - finalPayloadString = payloadStringWithCleanedRaw; - strippedFields.push("raw (attachment bodies removed)"); - console.log( - `✅ handleWebhookEndpoint - Removed attachment bodies from raw field, new size: ${payloadStringWithCleanedRaw.length} bytes`, - ); - } else { - // Still too large, also strip headers - const payloadWithCleanedRawAndNoHeaders = { - ...payloadWithCleanedRaw, - email: { - ...payloadWithCleanedRaw.email, - parsedData: { - ...enhancedParsedData, - raw: cleanedRaw, - headers: {}, - }, - }, - }; - const payloadStringWithCleanedRawAndNoHeaders = JSON.stringify( - payloadWithCleanedRawAndNoHeaders, - ); - finalPayload = payloadWithCleanedRawAndNoHeaders; - finalPayloadString = payloadStringWithCleanedRawAndNoHeaders; - strippedFields.push("raw (attachment bodies removed)", "headers"); - console.warn( - `⚠️ handleWebhookEndpoint - Also removed headers, final size: ${payloadStringWithCleanedRawAndNoHeaders.length} bytes`, - ); - } - } - - if (strippedFields.length > 0) { - console.log( - `📋 handleWebhookEndpoint - Cleaned payload for ${endpoint.name}: ${strippedFields.join(", ")}`, - ); - } - } + const { payloadString: finalPayloadString, strippedFields } = + ensurePayloadSize(webhookPayload); // Send the webhook const startTime = Date.now(); diff --git a/lib/email-management/email-thread-parser.test.ts b/lib/email-management/email-thread-parser.test.ts new file mode 100644 index 00000000..bb3a764d --- /dev/null +++ b/lib/email-management/email-thread-parser.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, it } from "bun:test"; +import { + parseTextEmailContent, + parseHtmlEmailContent, + parseEmailContent, + splitIntoMessages, +} from "@/lib/email-management/email-thread-parser"; + +describe("parseTextEmailContent", () => { + it("returns empty content for empty input", () => { + const result = parseTextEmailContent(""); + expect(result.newContent).toBe(""); + expect(result.quotedContent).toBe(""); + expect(result.hasQuotedContent).toBe(false); + expect(result.quoteLevels).toBe(0); + }); + + it("returns all content as new when no quotes present", () => { + const result = parseTextEmailContent("Hello, this is a plain email."); + expect(result.newContent).toBe("Hello, this is a plain email."); + expect(result.hasQuotedContent).toBe(false); + }); + + it("separates new content from Gmail-style attribution", () => { + const content = [ + "Thanks for the update!", + "", + "On Mon, 27 Jan 2025, John Doe wrote:", + "> Original message here", + ].join("\n"); + + const result = parseTextEmailContent(content); + expect(result.newContent).toBe("Thanks for the update!"); + expect(result.hasQuotedContent).toBe(true); + expect(result.quoteLevels).toBeGreaterThanOrEqual(1); + }); + + it("separates content from Outlook-style attribution", () => { + const content = [ + "Got it, thanks.", + "", + "----- Original Message -----", + "From: sender@example.com", + "Subject: Test", + ].join("\n"); + + const result = parseTextEmailContent(content); + expect(result.newContent).toBe("Got it, thanks."); + expect(result.hasQuotedContent).toBe(true); + }); + + it("detects > quote prefixes", () => { + const content = [ + "My reply", + "", + "> Previous message", + ">> Even older message", + ].join("\n"); + + const result = parseTextEmailContent(content); + expect(result.newContent).toBe("My reply"); + expect(result.hasQuotedContent).toBe(true); + expect(result.quoteLevels).toBeGreaterThanOrEqual(2); + }); + + it("detects mobile footers", () => { + const content = ["Short reply.", "", "Sent from my iPhone"].join("\n"); + + const result = parseTextEmailContent(content); + expect(result.newContent).toBe("Short reply."); + expect(result.hasQuotedContent).toBe(true); + }); + + it("handles Apple Mail forward format", () => { + const content = [ + "FYI see below.", + "", + "Begin forwarded message:", + "", + "From: someone@example.com", + ].join("\n"); + + const result = parseTextEmailContent(content); + expect(result.newContent).toBe("FYI see below."); + expect(result.hasQuotedContent).toBe(true); + }); +}); + +describe("parseHtmlEmailContent", () => { + it("returns empty content for empty input", () => { + const result = parseHtmlEmailContent(""); + expect(result.newContent).toBe(""); + expect(result.hasQuotedContent).toBe(false); + }); + + it("detects gmail_quote div", () => { + const html = + '
My reply
Quoted
'; + const result = parseHtmlEmailContent(html); + expect(result.newContent).toBe("
My reply
"); + expect(result.hasQuotedContent).toBe(true); + }); + + it("detects blockquote elements", () => { + const html = + "

New content

Old quoted content
"; + const result = parseHtmlEmailContent(html); + expect(result.newContent).toBe("

New content

"); + expect(result.hasQuotedContent).toBe(true); + }); + + it("detects border-left styled divs", () => { + const html = + '
Reply
Quoted
'; + const result = parseHtmlEmailContent(html); + expect(result.newContent).toBe("
Reply
"); + expect(result.hasQuotedContent).toBe(true); + }); + + it("counts nested blockquote levels", () => { + const html = + '
Reply
Level 1
Level 2
'; + const result = parseHtmlEmailContent(html); + expect(result.hasQuotedContent).toBe(true); + expect(result.quoteLevels).toBeGreaterThanOrEqual(2); + }); + + it("returns all content as new when no quotes", () => { + const html = "

Just a simple email.

"; + const result = parseHtmlEmailContent(html); + expect(result.newContent).toBe("

Just a simple email.

"); + expect(result.hasQuotedContent).toBe(false); + }); +}); + +describe("parseEmailContent", () => { + it("detects HTML and delegates to HTML parser", () => { + const html = "
Hello
Quoted
"; + const result = parseEmailContent(html); + expect(result.hasQuotedContent).toBe(true); + }); + + it("detects plain text and delegates to text parser", () => { + const text = "Reply\n\nOn Mon, Jan 27, 2025, user wrote:\n> old"; + const result = parseEmailContent(text); + expect(result.hasQuotedContent).toBe(true); + }); + + it("handles empty string", () => { + const result = parseEmailContent(""); + expect(result.newContent).toBe(""); + expect(result.hasQuotedContent).toBe(false); + }); + + it("detects HTML entities as HTML", () => { + const content = "Hello & World"; + const result = parseEmailContent(content); + // Should be treated as HTML-like due to entity + expect(result.newContent).toBeTruthy(); + }); +}); + +describe("splitIntoMessages", () => { + it("returns empty array for empty input", () => { + expect(splitIntoMessages("")).toEqual([]); + }); + + it("returns single message for plain text", () => { + const result = splitIntoMessages("Just a plain message."); + expect(result).toHaveLength(1); + expect(result[0].content).toBe("Just a plain message."); + expect(result[0].isForwarded).toBe(false); + }); + + it("splits on attribution lines", () => { + const content = [ + "My reply", + "", + "On Mon, 27 Jan 2025, John wrote:", + "Original message here", + ].join("\n"); + + const result = splitIntoMessages(content); + expect(result.length).toBeGreaterThanOrEqual(2); + expect(result[0].content).toBe("My reply"); + }); + + it("detects forwarded messages", () => { + const content = [ + "FYI", + "", + "---------- Forwarded message ----------", + "See below for details", + ].join("\n"); + + const result = splitIntoMessages(content); + expect(result.some((msg) => msg.isForwarded)).toBe(true); + }); +}); diff --git a/lib/email-management/sending-spike-detector.test.ts b/lib/email-management/sending-spike-detector.test.ts new file mode 100644 index 00000000..71620182 --- /dev/null +++ b/lib/email-management/sending-spike-detector.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "bun:test"; +import { + getUserAgeInDays, + getSeverity, + SPIKE_DETECTION_CONFIG, + type AwsReputationSnapshot, +} from "@/lib/email-management/sending-spike-detector"; + +describe("getUserAgeInDays", () => { + it("computes age from a Date object", () => { + const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); + const age = getUserAgeInDays(twoDaysAgo); + // Should be approximately 2 + expect(age).toBeGreaterThanOrEqual(1.9); + expect(age).toBeLessThanOrEqual(2.1); + }); + + it("computes age from an ISO string", () => { + const fiveDaysAgo = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000); + const age = getUserAgeInDays(fiveDaysAgo.toISOString()); + expect(age).toBeGreaterThanOrEqual(4.9); + expect(age).toBeLessThanOrEqual(5.1); + }); + + it("returns 0 for future date", () => { + const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000); + expect(getUserAgeInDays(tomorrow)).toBe(0); + }); + + it("returns 0 for current time", () => { + const now = new Date(); + const age = getUserAgeInDays(now); + expect(age).toBeGreaterThanOrEqual(0); + expect(age).toBeLessThan(0.01); + }); +}); + +describe("getSeverity", () => { + const noReputation = null; + + it("returns critical when 24h volume exceeds critical threshold", () => { + expect( + getSeverity( + 0, + 0, + SPIKE_DETECTION_CONFIG.ABSOLUTE_24H_CRITICAL, + noReputation, + ), + ).toBe("critical"); + }); + + it("returns critical when 1h volume exceeds critical threshold", () => { + expect( + getSeverity( + 0, + SPIKE_DETECTION_CONFIG.ABSOLUTE_1H_CRITICAL, + 0, + noReputation, + ), + ).toBe("critical"); + }); + + it("returns critical when reputation is at risk", () => { + const atRiskReputation = { isAtRisk: true } as AwsReputationSnapshot; + expect(getSeverity(0, 0, 0, atRiskReputation)).toBe("critical"); + }); + + it("returns high when 24h volume exceeds high threshold", () => { + expect( + getSeverity( + 0, + 0, + SPIKE_DETECTION_CONFIG.ABSOLUTE_24H_HIGH, + noReputation, + ), + ).toBe("high"); + }); + + it("returns high when 1h volume exceeds high threshold", () => { + expect( + getSeverity( + 0, + SPIKE_DETECTION_CONFIG.ABSOLUTE_1H_HIGH, + 0, + noReputation, + ), + ).toBe("high"); + }); + + it("returns medium for moderate volumes", () => { + expect(getSeverity(50, 100, 200, noReputation)).toBe("medium"); + }); + + it("returns medium when reputation is warning but not at risk", () => { + const warningReputation = { + isAtRisk: false, + isWarning: true, + } as AwsReputationSnapshot; + expect(getSeverity(50, 100, 200, warningReputation)).toBe("medium"); + }); +}); diff --git a/lib/email-management/sending-spike-detector.ts b/lib/email-management/sending-spike-detector.ts index bd898f7d..8d1868bf 100644 --- a/lib/email-management/sending-spike-detector.ts +++ b/lib/email-management/sending-spike-detector.ts @@ -7,7 +7,7 @@ import { redis } from "@/lib/redis" const SLACK_ADMIN_WEBHOOK_URL = process.env.SLACK_ADMIN_WEBHOOK_URL -const SPIKE_DETECTION_CONFIG = { +export const SPIKE_DETECTION_CONFIG = { HISTORICAL_DAYS: 14, SPIKE_THRESHOLD_MULTIPLIER: 8, MIN_HISTORICAL_DAILY_AVERAGE: 50, @@ -32,7 +32,7 @@ const SPIKE_AWS_REPUTATION_CACHE_KEY = "spike-alert:aws-reputation-cache" type AlertSeverity = "medium" | "high" | "critical" -type AwsReputationSnapshot = { +export type AwsReputationSnapshot = { latestBounceRatePercent: number latestComplaintRatePercent: number latestRejectRatePercent: number @@ -106,7 +106,7 @@ async function getUserInfo( return result[0] || null } -function getUserAgeInDays(createdAt: Date | string): number { +export function getUserAgeInDays(createdAt: Date | string): number { const created = createdAt instanceof Date ? createdAt : new Date(createdAt) return Math.max(0, (Date.now() - created.getTime()) / (1000 * 60 * 60 * 24)) } @@ -186,7 +186,7 @@ async function getAwsReputationSnapshot(): Promise } } -function getSeverity( +export function getSeverity( current15m: number, current1h: number, current24h: number, diff --git a/lib/email-management/warmup-limits.test.ts b/lib/email-management/warmup-limits.test.ts new file mode 100644 index 00000000..5c412b5a --- /dev/null +++ b/lib/email-management/warmup-limits.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "bun:test"; +import { getDailyLimitForAge } from "@/lib/email-management/warmup-limits"; + +describe("getDailyLimitForAge", () => { + it("day 1 → 20", () => { + expect(getDailyLimitForAge(1)).toBe(20); + }); + + it("day 2 → 40", () => { + expect(getDailyLimitForAge(2)).toBe(40); + }); + + it("day 3 → 75", () => { + expect(getDailyLimitForAge(3)).toBe(75); + }); + + it("day 4 falls into day-5 bucket → 150", () => { + expect(getDailyLimitForAge(4)).toBe(150); + }); + + it("day 5 → 150", () => { + expect(getDailyLimitForAge(5)).toBe(150); + }); + + it("day 6 falls into day-7 bucket → 300", () => { + expect(getDailyLimitForAge(6)).toBe(300); + }); + + it("day 7 → 300", () => { + expect(getDailyLimitForAge(7)).toBe(300); + }); + + it("day 8 falls into day-10 bucket → 500", () => { + expect(getDailyLimitForAge(8)).toBe(500); + }); + + it("day 10 → 500", () => { + expect(getDailyLimitForAge(10)).toBe(500); + }); + + it("day 11 falls into day-14 bucket → 1000", () => { + expect(getDailyLimitForAge(11)).toBe(1000); + }); + + it("day 14 → 1000", () => { + expect(getDailyLimitForAge(14)).toBe(1000); + }); + + it("day 15 (past warmup) → null", () => { + expect(getDailyLimitForAge(15)).toBeNull(); + }); + + it("day 100 (well past warmup) → null", () => { + expect(getDailyLimitForAge(100)).toBeNull(); + }); +}); diff --git a/lib/email-management/warmup-limits.ts b/lib/email-management/warmup-limits.ts index 514589a9..b00eb1fd 100644 --- a/lib/email-management/warmup-limits.ts +++ b/lib/email-management/warmup-limits.ts @@ -43,7 +43,7 @@ const DAILY_LIMITS: Record = { /** * Get the daily limit for a given account age */ -function getDailyLimitForAge(accountAgeInDays: number): number | null { +export function getDailyLimitForAge(accountAgeInDays: number): number | null { // After warmup period, return null (unlimited - rely on billing limits) if (accountAgeInDays > WARMUP_PERIOD_DAYS) { return null diff --git a/lib/email-management/webhook-payload.ts b/lib/email-management/webhook-payload.ts new file mode 100644 index 00000000..3a63c969 --- /dev/null +++ b/lib/email-management/webhook-payload.ts @@ -0,0 +1,150 @@ +/** + * Webhook payload construction and size management + * + * Extracted from email-router.ts to keep handleWebhookEndpoint focused on + * orchestration (delivery record management, HTTP request, logging). + */ + +import type { Endpoint } from "@/features/endpoints/types"; +import type { ParsedEmailData } from "./email-parser"; +import { sanitizeHtml } from "./email-parser"; + +// Maximum webhook payload size (1 MB safety margin) +export const MAX_WEBHOOK_PAYLOAD_SIZE = 1_000_000; + +interface EmailData { + structuredId: string; + messageId: string | null; + fromData: string | null; + toData: string | null; + recipient: string | null; + subject: string | null; + date: Date | null; + threadId: string | null; + threadPosition: number | null; +} + +/** + * Build the webhook payload from structured email data. + */ +export function constructWebhookPayload( + emailData: EmailData, + parsedEmailData: ParsedEmailData, + attachmentsWithUrls: Array>, + endpoint: Pick, +) { + const enhancedParsedData = { + ...parsedEmailData, + attachments: attachmentsWithUrls, + }; + + return { + event: "email.received" as const, + timestamp: new Date().toISOString(), + email: { + id: emailData.structuredId, + messageId: emailData.messageId, + from: emailData.fromData ? JSON.parse(emailData.fromData) : null, + to: emailData.toData ? JSON.parse(emailData.toData) : null, + recipient: emailData.recipient, + subject: emailData.subject, + receivedAt: emailData.date, + + threadId: emailData.threadId || null, + threadPosition: emailData.threadPosition || null, + + parsedData: enhancedParsedData, + + cleanedContent: { + html: parsedEmailData.htmlBody + ? sanitizeHtml(parsedEmailData.htmlBody) + : null, + text: parsedEmailData.textBody || null, + hasHtml: !!parsedEmailData.htmlBody, + hasText: !!parsedEmailData.textBody, + attachments: attachmentsWithUrls, + headers: parsedEmailData.headers || {}, + }, + }, + endpoint: { + id: endpoint.id, + name: endpoint.name, + type: endpoint.type, + }, + }; +} + +/** + * Ensure the serialised payload fits within `maxSize` bytes. + * + * Strategy (applied in order until the payload fits): + * 1. Strip base64-encoded attachment bodies from the `raw` field. + * 2. Also strip the `headers` object from `parsedData`. + * + * Returns the (possibly reduced) payload string and a list of field names + * that were stripped. + */ +export function ensurePayloadSize( + webhookPayload: ReturnType, + maxSize: number = MAX_WEBHOOK_PAYLOAD_SIZE, +): { payloadString: string; strippedFields: string[] } { + const payloadString = JSON.stringify(webhookPayload); + const strippedFields: string[] = []; + + if (payloadString.length <= maxSize) { + return { payloadString, strippedFields }; + } + + console.warn( + `⚠️ Webhook payload too large (${payloadString.length} bytes), stripping attachment bodies from raw field`, + ); + + const rawField = webhookPayload.email.parsedData.raw; + if (!rawField) { + return { payloadString, strippedFields }; + } + + // Remove base64-encoded attachment bodies while preserving MIME structure + const cleanedRaw = rawField.replace( + /Content-Transfer-Encoding:\s*base64\s*[\r\n]+[\r\n]+([\s\S]+?)(?=\r?\n--|\r?\n\r?\nContent-|$)/gi, + "Content-Transfer-Encoding: base64\r\n\r\n[binary attachment data removed - use Attachments API]\r\n", + ); + + const payloadWithCleanedRaw = { + ...webhookPayload, + email: { + ...webhookPayload.email, + parsedData: { + ...webhookPayload.email.parsedData, + raw: cleanedRaw, + }, + }, + }; + const payloadStringWithCleanedRaw = JSON.stringify(payloadWithCleanedRaw); + + if (payloadStringWithCleanedRaw.length <= maxSize) { + strippedFields.push("raw (attachment bodies removed)"); + console.log( + `✅ Removed attachment bodies from raw field, new size: ${payloadStringWithCleanedRaw.length} bytes`, + ); + return { payloadString: payloadStringWithCleanedRaw, strippedFields }; + } + + // Still too large — also strip headers + const payloadWithNoHeaders = { + ...payloadWithCleanedRaw, + email: { + ...payloadWithCleanedRaw.email, + parsedData: { + ...payloadWithCleanedRaw.email.parsedData, + headers: {}, + }, + }, + }; + const finalString = JSON.stringify(payloadWithNoHeaders); + strippedFields.push("raw (attachment bodies removed)", "headers"); + console.warn( + `⚠️ Also removed headers, final size: ${finalString.length} bytes`, + ); + return { payloadString: finalString, strippedFields }; +} diff --git a/lib/guard/rule-matcher.test.ts b/lib/guard/rule-matcher.test.ts new file mode 100644 index 00000000..bf863573 --- /dev/null +++ b/lib/guard/rule-matcher.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "bun:test"; +import { + checkStringCriteria, + checkEmailCriteria, +} from "@/lib/guard/rule-matcher"; + +describe("checkStringCriteria", () => { + describe("OR operator", () => { + it("matches when any value is found", () => { + expect( + checkStringCriteria("hello world", ["hello", "goodbye"], "OR"), + ).toBe(true); + }); + + it("does not match when no values found", () => { + expect(checkStringCriteria("hello world", ["foo", "bar"], "OR")).toBe( + false, + ); + }); + }); + + describe("AND operator", () => { + it("matches when all values are found", () => { + expect( + checkStringCriteria("hello world foo", ["hello", "world"], "AND"), + ).toBe(true); + }); + + it("does not match when some values are missing", () => { + expect( + checkStringCriteria("hello world", ["hello", "missing"], "AND"), + ).toBe(false); + }); + }); + + it("lowercases pattern values for matching", () => { + // The function lowercases values but expects content to already be lowercased + expect(checkStringCriteria("hello world", ["HELLO"], "OR")).toBe(true); + }); +}); + +describe("checkEmailCriteria", () => { + describe("exact match", () => { + it("matches exact email (OR)", () => { + expect( + checkEmailCriteria( + ["user@example.com"], + ["user@example.com"], + "OR", + ), + ).toBe(true); + }); + + it("does not match different email", () => { + expect( + checkEmailCriteria( + ["user@example.com"], + ["other@example.com"], + "OR", + ), + ).toBe(false); + }); + }); + + describe("wildcard patterns", () => { + it("matches *@domain.com pattern", () => { + expect( + checkEmailCriteria( + ["anyone@example.com"], + ["*@example.com"], + "OR", + ), + ).toBe(true); + }); + + it("does not match wrong domain with wildcard", () => { + expect( + checkEmailCriteria( + ["anyone@other.com"], + ["*@example.com"], + "OR", + ), + ).toBe(false); + }); + }); + + describe("OR operator", () => { + it("matches if any pattern matches", () => { + expect( + checkEmailCriteria( + ["user@example.com"], + ["nope@nope.com", "*@example.com"], + "OR", + ), + ).toBe(true); + }); + }); + + describe("AND operator", () => { + it("matches when all patterns match", () => { + expect( + checkEmailCriteria( + ["user@example.com", "admin@example.com"], + ["user@example.com", "*@example.com"], + "AND", + ), + ).toBe(true); + }); + + it("does not match when a pattern has no match", () => { + expect( + checkEmailCriteria( + ["user@example.com"], + ["user@example.com", "admin@other.com"], + "AND", + ), + ).toBe(false); + }); + }); + + it("lowercases pattern for matching", () => { + // The function lowercases patterns but expects email addresses to already be lowercased + expect( + checkEmailCriteria( + ["user@example.com"], + ["USER@EXAMPLE.COM"], + "OR", + ), + ).toBe(true); + }); +}); diff --git a/lib/guard/rule-matcher.ts b/lib/guard/rule-matcher.ts index 3180722d..b5e43b76 100644 --- a/lib/guard/rule-matcher.ts +++ b/lib/guard/rule-matcher.ts @@ -222,7 +222,7 @@ async function checkExplicitRule( /** * Check string-based criteria (subject, hasWords) */ -function checkStringCriteria( +export function checkStringCriteria( content: string, values: string[], operator: 'OR' | 'AND' @@ -237,7 +237,7 @@ function checkStringCriteria( /** * Check email-based criteria (from) with wildcard support */ -function checkEmailCriteria( +export function checkEmailCriteria( emailAddresses: string[], patterns: string[], operator: 'OR' | 'AND' diff --git a/lib/ses-monitoring/rate-tracker.ts b/lib/ses-monitoring/rate-tracker.ts index 0d34e65b..d01ed493 100644 --- a/lib/ses-monitoring/rate-tracker.ts +++ b/lib/ses-monitoring/rate-tracker.ts @@ -11,6 +11,7 @@ import { and, count, eq, gte } from "drizzle-orm"; import { nanoid } from "nanoid"; import { db } from "@/lib/db"; import { emailDeliveryEvents, sentEmails, sesTenants } from "@/lib/db/schema"; +import { extractDomainFromEmail } from "@/lib/utils/email-utils"; // Rate thresholds for tenant alerting and automatic suspension export const RATE_THRESHOLDS = { @@ -93,7 +94,7 @@ export async function storeSESEvent(params: { bounceSubType: params.bounceSubType, diagnosticCode: params.diagnosticCode, failedRecipient: params.recipient, - failedRecipientDomain: params.recipient.split("@")[1] || null, + failedRecipientDomain: extractDomainFromEmail(params.recipient) || null, originalMessageId: params.messageId, userId: tenant?.userId || null, tenantId: tenant?.id || null, diff --git a/lib/utils/attachment-utils.test.ts b/lib/utils/attachment-utils.test.ts new file mode 100644 index 00000000..9cce2593 --- /dev/null +++ b/lib/utils/attachment-utils.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "bun:test"; +import { + inferContentType, + normalizeAttachmentContentType, + normalizeAttachments, +} from "@/lib/utils/attachment-utils"; + +describe("inferContentType", () => { + it.each([ + ["file.pdf", "application/pdf"], + ["file.jpg", "image/jpeg"], + ["file.jpeg", "image/jpeg"], + ["file.png", "image/png"], + ["file.gif", "image/gif"], + ["file.txt", "text/plain"], + ["file.html", "text/html"], + ["file.json", "application/json"], + ["file.zip", "application/zip"], + ["file.doc", "application/msword"], + [ + "file.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ], + ["file.xls", "application/vnd.ms-excel"], + [ + "file.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ], + ])("infers %s → %s", (filename: string, expected: string) => { + expect(inferContentType(filename)).toBe(expected); + }); + + it("returns application/octet-stream for unknown extension", () => { + expect(inferContentType("file.xyz")).toBe("application/octet-stream"); + }); + + it("returns application/octet-stream for no filename", () => { + expect(inferContentType(undefined)).toBe("application/octet-stream"); + }); + + it("returns application/octet-stream for filename without extension", () => { + expect(inferContentType("README")).toBe("application/octet-stream"); + }); +}); + +describe("normalizeAttachmentContentType", () => { + it("infers contentType when missing", () => { + const result = normalizeAttachmentContentType({ filename: "doc.pdf" }); + expect(result.contentType).toBe("application/pdf"); + }); + + it("preserves existing contentType", () => { + const result = normalizeAttachmentContentType({ + filename: "doc.pdf", + contentType: "custom/type", + }); + expect(result.contentType).toBe("custom/type"); + }); + + it("maps snake_case content_type to camelCase", () => { + const result = normalizeAttachmentContentType({ + filename: "img.png", + content_type: "image/png", + }); + expect(result.contentType).toBe("image/png"); + }); + + it("prefers contentType over content_type", () => { + const result = normalizeAttachmentContentType({ + filename: "x.txt", + contentType: "text/plain", + content_type: "wrong/type", + }); + expect(result.contentType).toBe("text/plain"); + }); +}); + +describe("normalizeAttachments", () => { + it("normalizes an array of attachments", () => { + const result = normalizeAttachments([ + { filename: "a.pdf" }, + { filename: "b.png", contentType: "image/png" }, + { filename: "c.doc", content_type: "application/msword" }, + ]); + expect(result).toHaveLength(3); + expect(result[0].contentType).toBe("application/pdf"); + expect(result[1].contentType).toBe("image/png"); + expect(result[2].contentType).toBe("application/msword"); + }); + + it("handles empty array", () => { + expect(normalizeAttachments([])).toEqual([]); + }); +}); diff --git a/lib/utils/attachment-utils.ts b/lib/utils/attachment-utils.ts new file mode 100644 index 00000000..49f27639 --- /dev/null +++ b/lib/utils/attachment-utils.ts @@ -0,0 +1,82 @@ +/** + * Attachment normalization utilities + * + * Ensures attachments have a valid contentType by inferring from filename extension. + * Handles snake_case `content_type` → camelCase `contentType` mapping. + */ + +interface RawAttachment { + filename?: string; + contentType?: string; + content_type?: string; + [key: string]: unknown; +} + +const EXTENSION_CONTENT_TYPES: Record = { + pdf: "application/pdf", + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + gif: "image/gif", + txt: "text/plain", + html: "text/html", + json: "application/json", + zip: "application/zip", + doc: "application/msword", + docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + xls: "application/vnd.ms-excel", + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +}; + +/** + * Infer content type from a filename extension. + * Returns `application/octet-stream` when the extension is unknown or missing. + */ +export function inferContentType(filename: string | undefined): string { + if (!filename) return "application/octet-stream"; + const ext = filename.toLowerCase().split(".").pop(); + return (ext && EXTENSION_CONTENT_TYPES[ext]) || "application/octet-stream"; +} + +/** + * Normalize a single attachment: ensure `contentType` is set. + */ +export function normalizeAttachmentContentType( + att: T, + index?: number, +): T & { contentType: string } { + if (!att.contentType && !att.content_type) { + if (index !== undefined) { + console.log( + `⚠️ Attachment ${index + 1} missing contentType, using fallback`, + ); + } + return { + ...att, + contentType: inferContentType(att.filename), + }; + } + + return { + ...att, + contentType: (att.contentType || att.content_type) as string, + }; +} + +/** + * Normalize an array of attachments. + * Overloaded: when called with untyped data (e.g. JSON.parse output), + * returns the same permissive type so callers aren't broken. + */ +// biome-ignore lint/suspicious/noExplicitAny: JSON.parse callers pass any[] +export function normalizeAttachments(rawAttachments: any[]): any[]; +export function normalizeAttachments( + rawAttachments: T[], +): Array; +export function normalizeAttachments( + rawAttachments: RawAttachment[], +): Array { + return rawAttachments.map((att, index) => + normalizeAttachmentContentType(att, index), + ); +} diff --git a/lib/utils/email-utils.test.ts b/lib/utils/email-utils.test.ts new file mode 100644 index 00000000..4eb878a1 --- /dev/null +++ b/lib/utils/email-utils.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "bun:test"; +import { + extractDomainFromEmail, + extractEmailAddress, + extractEmailName, + isValidEmail, +} from "@/lib/utils/email-utils"; + +describe("isValidEmail", () => { + it("accepts a plain email", () => { + expect(isValidEmail("user@example.com")).toBe(true); + }); + + it("accepts subdomains", () => { + expect(isValidEmail("user@mail.example.com")).toBe(true); + }); + + it("rejects empty string", () => { + expect(isValidEmail("")).toBe(false); + }); + + it("rejects missing @", () => { + expect(isValidEmail("userexample.com")).toBe(false); + }); + + it("rejects spaces", () => { + expect(isValidEmail("user @example.com")).toBe(false); + }); + + it("rejects missing domain", () => { + expect(isValidEmail("user@")).toBe(false); + }); + + it("rejects missing local part", () => { + expect(isValidEmail("@example.com")).toBe(false); + }); +}); + +describe("extractDomainFromEmail", () => { + it("extracts domain from a plain email", () => { + expect(extractDomainFromEmail("user@Example.COM")).toBe("example.com"); + }); + + it("extracts domain from Name format", () => { + expect(extractDomainFromEmail("John Doe ")).toBe( + "domain.org", + ); + }); + + it("returns empty string when no @ present", () => { + expect(extractDomainFromEmail("nodomain")).toBe(""); + }); + + it("returns empty string for empty input", () => { + expect(extractDomainFromEmail("")).toBe(""); + }); +}); + +describe("extractEmailAddress", () => { + it("extracts email from Name format", () => { + expect(extractEmailAddress("John ")).toBe( + "john@example.com", + ); + }); + + it("returns plain email unchanged", () => { + expect(extractEmailAddress("user@example.com")).toBe("user@example.com"); + }); + + it("handles quoted name with angle brackets", () => { + expect(extractEmailAddress('"John Doe" ')).toBe("jd@x.com"); + }); +}); + +describe("extractEmailName", () => { + it("extracts unquoted name", () => { + expect(extractEmailName("John Doe ")).toBe("John Doe"); + }); + + it("strips surrounding quotes from name", () => { + expect(extractEmailName('"John Doe" ')).toBe("John Doe"); + }); + + it("strips surrounding single quotes", () => { + expect(extractEmailName("'Jane' ")).toBe("Jane"); + }); + + it("returns null for a plain email", () => { + expect(extractEmailName("user@example.com")).toBeNull(); + }); + + it("returns null for empty string", () => { + expect(extractEmailName("")).toBeNull(); + }); +}); diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts new file mode 100644 index 00000000..9de925e3 --- /dev/null +++ b/lib/utils/email-utils.ts @@ -0,0 +1,57 @@ +/** + * Shared email utilities + * + * Consolidates common email operations that were duplicated across the codebase: + * - Email validation + * - Domain extraction from email addresses + * - Email address extraction from "Name " format + * - Name extraction from "Name " format + */ + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +/** + * Validate an email address format + */ +export function isValidEmail(email: string): boolean { + return EMAIL_REGEX.test(email); +} + +/** + * Extract domain from email address, handling "Name " format. + * Returns lowercase domain, or empty string if no domain found. + */ +export function extractDomainFromEmail(email: string): string { + // Fast path: check for angle brackets only if present + const angleMatch = email.indexOf("<"); + const source = + angleMatch !== -1 + ? email.slice(angleMatch + 1, email.indexOf(">", angleMatch)) + : email; + const atIndex = source.lastIndexOf("@"); + return atIndex !== -1 ? source.slice(atIndex + 1).toLowerCase() : ""; +} + +/** + * Extract email address from formatted email (removes name part). + * Handles "Name " and plain "email@domain.com" formats. + */ +export function extractEmailAddress(email: string): string { + const emailMatch = email.match(/<([^>]+)>/); + if (emailMatch) { + return emailMatch[1]; + } + return email; +} + +/** + * Extract name from formatted email (removes email part). + * Returns null if no name part found. + */ +export function extractEmailName(email: string): string | null { + const nameMatch = email.match(/^(.+?)\s*<[^>]+>$/); + if (nameMatch) { + return nameMatch[1].trim().replace(/^["']|["']$/g, ""); + } + return null; +} diff --git a/package.json b/package.json index d160e979..00046a23 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "@aws-sdk/client-ses": "^3.817.0", "@aws-sdk/client-sesv2": "^3.883.0", "@aws-sdk/client-sns": "^3.940.0", - "@better-auth/passkey": "^1.4.5", + "@better-auth/infra": "^0.1.8", + "@better-auth/api-key": "^1.5.0", + "@better-auth/passkey": "^1.5.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -110,7 +112,7 @@ "async_hooks": "^1.0.0", "atmn": "^0.0.22", "autumn-js": "0.1.48", - "better-auth": "^1.4.4", + "better-auth": "^1.5.0", "bun-types": "^1.2.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -183,6 +185,15 @@ "typescript": "^5" }, "overrides": { + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@better-auth/core": "1.5.0" + } +} + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@better-auth/core": "1.5.0" + } "@types/react": "19.2.7", "@types/react-dom": "19.2.3" } From aeb43fe1a1c72205dfadec496b4588e0f1bec826 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Sun, 1 Mar 2026 17:32:15 -0500 Subject: [PATCH 2/2] fix: remove malformed duplicate JSON in package manifest --- package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package.json b/package.json index 00046a23..90e565bf 100644 --- a/package.json +++ b/package.json @@ -189,12 +189,4 @@ "@types/react-dom": "19.2.3", "@better-auth/core": "1.5.0" } -} - "@types/react": "19.2.7", - "@types/react-dom": "19.2.3", - "@better-auth/core": "1.5.0" - } - "@types/react": "19.2.7", - "@types/react-dom": "19.2.3" - } }