export interface IAttachment { filename: string; content: Buffer; contentType: string; contentId?: string; // Optional content ID for inline attachments encoding?: string; // Optional encoding specification } export interface IEmailOptions { from: string; to: string | string[]; // Support multiple recipients cc?: string | string[]; // Optional CC recipients bcc?: string | string[]; // Optional BCC recipients subject: string; text: string; html?: string; // Optional HTML version attachments?: IAttachment[]; headers?: Record; // Optional additional headers mightBeSpam?: boolean; priority?: 'high' | 'normal' | 'low'; // Optional email priority } export class Email { from: string; to: string[]; cc: string[]; bcc: string[]; subject: string; text: string; html?: string; attachments: IAttachment[]; headers: Record; mightBeSpam: boolean; priority: 'high' | 'normal' | 'low'; constructor(options: IEmailOptions) { // Validate and set the from address if (!this.isValidEmail(options.from)) { throw new Error(`Invalid sender email address: ${options.from}`); } this.from = options.from; // Handle to addresses (single or multiple) this.to = this.parseRecipients(options.to); // Handle optional cc and bcc this.cc = options.cc ? this.parseRecipients(options.cc) : []; this.bcc = options.bcc ? this.parseRecipients(options.bcc) : []; // Validate that we have at least one recipient if (this.to.length === 0 && this.cc.length === 0 && this.bcc.length === 0) { throw new Error('Email must have at least one recipient'); } // Set subject with sanitization this.subject = this.sanitizeString(options.subject || ''); // Set text content with sanitization this.text = this.sanitizeString(options.text || ''); // Set optional HTML content this.html = options.html ? this.sanitizeString(options.html) : undefined; // Set attachments this.attachments = Array.isArray(options.attachments) ? options.attachments : []; // Set additional headers this.headers = options.headers || {}; // Set spam flag this.mightBeSpam = options.mightBeSpam || false; // Set priority this.priority = options.priority || 'normal'; } /** * Validates an email address using a regex pattern * @param email The email address to validate * @returns boolean indicating if the email is valid */ private isValidEmail(email: string): boolean { if (!email || typeof email !== 'string') return false; // Basic but effective email regex const emailRegex = /^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}$/; return emailRegex.test(email); } /** * Parses and validates recipient email addresses * @param recipients A string or array of recipient emails * @returns Array of validated email addresses */ private parseRecipients(recipients: string | string[]): string[] { const result: string[] = []; if (typeof recipients === 'string') { // Handle single recipient if (this.isValidEmail(recipients)) { result.push(recipients); } else { throw new Error(`Invalid recipient email address: ${recipients}`); } } else if (Array.isArray(recipients)) { // Handle multiple recipients for (const recipient of recipients) { if (this.isValidEmail(recipient)) { result.push(recipient); } else { throw new Error(`Invalid recipient email address: ${recipient}`); } } } return result; } /** * Basic sanitization for strings to prevent header injection * @param input The string to sanitize * @returns Sanitized string */ private sanitizeString(input: string): string { if (!input) return ''; // Remove CR and LF characters to prevent header injection return input.replace(/\r|\n/g, ' '); } /** * Gets the domain part of the from email address * @returns The domain part of the from email or null if invalid */ public getFromDomain(): string | null { try { const parts = this.from.split('@'); if (parts.length !== 2 || !parts[1]) { return null; } return parts[1]; } catch (error) { console.error('Error extracting domain from email:', error); return null; } } /** * Gets all recipients (to, cc, bcc) as a unique array * @returns Array of all unique recipient email addresses */ public getAllRecipients(): string[] { // Combine all recipients and remove duplicates return [...new Set([...this.to, ...this.cc, ...this.bcc])]; } /** * Gets primary recipient (first in the to field) * @returns The primary recipient email or null if none exists */ public getPrimaryRecipient(): string | null { return this.to.length > 0 ? this.to[0] : null; } /** * Checks if the email has attachments * @returns Boolean indicating if the email has attachments */ public hasAttachments(): boolean { return this.attachments.length > 0; } /** * Gets the total size of all attachments in bytes * @returns Total size of all attachments in bytes */ public getAttachmentsSize(): number { return this.attachments.reduce((total, attachment) => { return total + (attachment.content?.length || 0); }, 0); } /** * Creates an RFC822 compliant email string * @returns The email formatted as an RFC822 compliant string */ public toRFC822String(): string { // This is a simplified version - a complete implementation would be more complex let result = ''; // Add headers result += `From: ${this.from}\r\n`; result += `To: ${this.to.join(', ')}\r\n`; if (this.cc.length > 0) { result += `Cc: ${this.cc.join(', ')}\r\n`; } result += `Subject: ${this.subject}\r\n`; result += `Date: ${new Date().toUTCString()}\r\n`; // Add custom headers for (const [key, value] of Object.entries(this.headers)) { result += `${key}: ${value}\r\n`; } // Add priority if not normal if (this.priority !== 'normal') { const priorityValue = this.priority === 'high' ? '1' : '5'; result += `X-Priority: ${priorityValue}\r\n`; } // Add content type and body result += `Content-Type: text/plain; charset=utf-8\r\n`; result += `\r\n${this.text}\r\n`; return result; } }