Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
};
Comment on lines 51 to 58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🟠 [LangGraph v3] Replace console.error with a logging utility to avoid exposing sensitive information.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
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);
}
});
};
try {
const email = prepare();
resolve(email.sendEmail());
} catch (e) {
loggingUtility.error('Email preparation failed', { error: e });
reject(e);
}

Expand All @@ -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,
Expand All @@ -74,7 +85,7 @@ const _sendScheduledEmailsAndSMS = async (
const formattedCalEvent = formatCalEvent(calEvent);
const emailsToSend: Promise<unknown>[] = [];

if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) {
if (!hostEmailDisabled && shouldSendEmail(eventTypeMetadata, 'host')) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent })));

if (formattedCalEvent.team) {
Expand All @@ -86,7 +97,7 @@ const _sendScheduledEmailsAndSMS = async (
}
}

if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
if (!attendeeEmailDisabled && shouldSendEmail(eventTypeMetadata, 'attendee')) {
emailsToSend.push(
...formattedCalEvent.attendees.map((attendee) => {
return sendEmail(
Expand Down
80 changes: 80 additions & 0 deletions packages/emails/lib/emailHelpers.ts
Original file line number Diff line number Diff line change
@@ -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[^>]*>.*?<\/script>/gi, "")
.replace(/<iframe[^>]*>.*?<\/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;
}
Comment on lines +1 to +80

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🟠 [LangGraph v3] The recipientEmail and eventType parameters in shouldRateLimitEmail are defined but never used, leading to unnecessary code clutter. Consider removing these parameters if they are not needed.

📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
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[^>]*>.*?<\/script>/gi, "")
.replace(/<iframe[^>]*>.*?<\/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;
}
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[^>]*>.*?<\/script>/gi, "")
.replace(/<iframe[^>]*>.*?<\/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
* @returns boolean indicating if email should be sent
*/
export function shouldRateLimitEmail(): 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;
}