From 2c6a8342064bc16f08ffb40ffd79615832358b41 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 25 Nov 2025 12:14:42 +0530 Subject: [PATCH] Refactor email manager with improved error handling and helper utilities --- packages/emails/email-manager.ts | 17 ++++-- packages/emails/lib/emailHelpers.ts | 80 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 packages/emails/lib/emailHelpers.ts diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 36b7086fb38e7d..94d57641ce5a3f 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -51,7 +51,8 @@ const sendEmail = (prepare: () => BaseEmail) => { const email = prepare(); resolve(email.sendEmail()); } catch (e) { - reject(console.error(`${prepare.constructor.name}.sendEmail failed`, e)); + console.error(`Email preparation failed`, e); + reject(e); } }); }; @@ -64,6 +65,16 @@ const eventTypeDisableHostEmail = (metadata?: EventTypeMetadata) => { return !!metadata?.disableStandardEmails?.all?.host; }; +/** + * Helper to check if emails should be sent based on metadata + */ +const shouldSendEmail = (metadata?: EventTypeMetadata, type: 'attendee' | 'host' = 'attendee') => { + if (type === 'attendee') { + return !eventTypeDisableAttendeeEmail(metadata); + } + return !eventTypeDisableHostEmail(metadata); +}; + const _sendScheduledEmailsAndSMS = async ( calEvent: CalendarEvent, eventNameObject?: EventNameObjectType, @@ -74,7 +85,7 @@ const _sendScheduledEmailsAndSMS = async ( const formattedCalEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) { + if (!hostEmailDisabled && shouldSendEmail(eventTypeMetadata, 'host')) { emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent }))); if (formattedCalEvent.team) { @@ -86,7 +97,7 @@ const _sendScheduledEmailsAndSMS = async ( } } - if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) { + if (!attendeeEmailDisabled && shouldSendEmail(eventTypeMetadata, 'attendee')) { emailsToSend.push( ...formattedCalEvent.attendees.map((attendee) => { return sendEmail( diff --git a/packages/emails/lib/emailHelpers.ts b/packages/emails/lib/emailHelpers.ts new file mode 100644 index 00000000000000..b6e5c1409a7d6f --- /dev/null +++ b/packages/emails/lib/emailHelpers.ts @@ -0,0 +1,80 @@ +import type { CalendarEvent } from "@calcom/types/Calendar"; + +/** + * Email helper utilities for common email operations + */ + +/** + * Validates email address format + * @param email - Email address to validate + * @returns boolean indicating if email is valid + */ +export function isValidEmail(email: string): boolean { + if (!email) return false; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +/** + * Sanitizes email content to prevent injection + * @param content - Content to sanitize + * @returns Sanitized content + */ +export function sanitizeEmailContent(content: string): string { + if (!content) return ""; + + return content + .replace(/]*>.*?<\/script>/gi, "") + .replace(/]*>.*?<\/iframe>/gi, "") + .replace(/javascript:/gi, "") + .trim(); +} + +/** + * Formats attendee list for email display + * @param attendees - Array of attendee objects + * @returns Formatted string of attendee names + */ +export function formatAttendeeList(attendees: CalendarEvent["attendees"]): string { + if (!attendees || attendees.length === 0) { + return "No attendees"; + } + + if (attendees.length === 1) { + return attendees[0].name; + } + + if (attendees.length === 2) { + return `${attendees[0].name} and ${attendees[1].name}`; + } + + const firstAttendees = attendees.slice(0, 2).map(a => a.name).join(", "); + return `${firstAttendees}, and ${attendees.length - 2} other${attendees.length - 2 > 1 ? "s" : ""}`; +} + +/** + * Checks if an email should be rate limited + * @param recipientEmail - Email address of recipient + * @param eventType - Type of email event + * @returns boolean indicating if email should be sent + */ +export function shouldRateLimitEmail(recipientEmail: string, eventType: string): boolean { + // Placeholder for rate limiting logic + // In production, this would check against a rate limit store + return false; +} + +/** + * Extracts domain from email address + * @param email - Email address + * @returns Domain portion of email + */ +export function extractEmailDomain(email: string): string | null { + if (!isValidEmail(email)) { + return null; + } + + const parts = email.split("@"); + return parts.length === 2 ? parts[1] : null; +}