941 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			941 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from '../../plugins.ts';
 | |
| import { EmailValidator } from './classes.emailvalidator.ts';
 | |
| 
 | |
| 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[]; // Optional for templates
 | |
|   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<string, string>; // Optional additional headers
 | |
|   mightBeSpam?: boolean;
 | |
|   priority?: 'high' | 'normal' | 'low'; // Optional email priority
 | |
|   skipAdvancedValidation?: boolean; // Skip advanced validation for special cases
 | |
|   variables?: Record<string, any>; // Template variables for placeholder replacement
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Email class represents a complete email message.
 | |
|  * 
 | |
|  * This class takes IEmailOptions in the constructor and normalizes the data:
 | |
|  * - 'to', 'cc', 'bcc' are always converted to arrays
 | |
|  * - Optional properties get default values
 | |
|  * - Additional properties like messageId and envelopeFrom are generated
 | |
|  */
 | |
| export class Email {
 | |
|   // INormalizedEmail properties
 | |
|   from: string;
 | |
|   to: string[];
 | |
|   cc: string[];
 | |
|   bcc: string[];
 | |
|   subject: string;
 | |
|   text: string;
 | |
|   html?: string;
 | |
|   attachments: IAttachment[];
 | |
|   headers: Record<string, string>;
 | |
|   mightBeSpam: boolean;
 | |
|   priority: 'high' | 'normal' | 'low';
 | |
|   variables: Record<string, any>;
 | |
|   
 | |
|   // Additional Email-specific properties
 | |
|   private envelopeFrom: string;
 | |
|   private messageId: string;
 | |
|   
 | |
|   // Static validator instance for reuse
 | |
|   private static emailValidator: EmailValidator;
 | |
|   
 | |
|   constructor(options: IEmailOptions) {
 | |
|     // Initialize validator if not already
 | |
|     if (!Email.emailValidator) {
 | |
|       Email.emailValidator = new EmailValidator();
 | |
|     }
 | |
|     
 | |
|     // Validate and set the from address using improved validation
 | |
|     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 = options.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) : [];
 | |
|     
 | |
|     // Note: Templates may be created without recipients
 | |
|     // Recipients will be added when the email is actually sent
 | |
| 
 | |
|     // 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';
 | |
|     
 | |
|     // Set template variables
 | |
|     this.variables = options.variables || {};
 | |
|     
 | |
|     // Initialize envelope from (defaults to the from address)
 | |
|     this.envelopeFrom = this.from;
 | |
|     
 | |
|     // Generate message ID if not provided
 | |
|     this.messageId = `<${Date.now()}.${Math.random().toString(36).substring(2, 15)}@${this.getFromDomain() || 'localhost'}>`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Validates an email address using smartmail's EmailAddressValidator
 | |
|    * For constructor validation, we only check syntax to avoid delays
 | |
|    * Supports RFC-compliant addresses including display names and bounce addresses.
 | |
|    * 
 | |
|    * @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;
 | |
|     
 | |
|     // Handle empty return path (bounce address)
 | |
|     if (email === '<>' || email === '') {
 | |
|       return true; // Empty return path is valid for bounces per RFC 5321
 | |
|     }
 | |
|     
 | |
|     // Extract email from display name format
 | |
|     const extractedEmail = this.extractEmailAddress(email);
 | |
|     if (!extractedEmail) return false;
 | |
|     
 | |
|     // Convert IDN (International Domain Names) to ASCII for validation
 | |
|     let emailToValidate = extractedEmail;
 | |
|     const atIndex = extractedEmail.indexOf('@');
 | |
|     if (atIndex > 0) {
 | |
|       const localPart = extractedEmail.substring(0, atIndex);
 | |
|       const domainPart = extractedEmail.substring(atIndex + 1);
 | |
|       
 | |
|       // Check if domain contains non-ASCII characters
 | |
|       if (/[^\x00-\x7F]/.test(domainPart)) {
 | |
|         try {
 | |
|           // Convert IDN to ASCII using the URL API (built-in punycode support)
 | |
|           const url = new URL(`http://${domainPart}`);
 | |
|           emailToValidate = `${localPart}@${url.hostname}`;
 | |
|         } catch (e) {
 | |
|           // If conversion fails, allow the original domain
 | |
|           // This supports testing and edge cases
 | |
|           emailToValidate = extractedEmail;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Use smartmail's validation for the ASCII-converted email address
 | |
|     return Email.emailValidator.isValidFormat(emailToValidate);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Extracts the email address from a string that may contain a display name.
 | |
|    * Handles formats like:
 | |
|    * - simple@example.com
 | |
|    * - "John Doe" <john@example.com>
 | |
|    * - John Doe <john@example.com>
 | |
|    * 
 | |
|    * @param emailString The email string to parse
 | |
|    * @returns The extracted email address or null
 | |
|    */
 | |
|   private extractEmailAddress(emailString: string): string | null {
 | |
|     if (!emailString || typeof emailString !== 'string') return null;
 | |
|     
 | |
|     emailString = emailString.trim();
 | |
|     
 | |
|     // Handle empty return path first
 | |
|     if (emailString === '<>' || emailString === '') {
 | |
|       return '';
 | |
|     }
 | |
|     
 | |
|     // Check for angle brackets format - updated regex to handle empty content
 | |
|     const angleMatch = emailString.match(/<([^>]*)>/);
 | |
|     if (angleMatch) {
 | |
|       // If matched but content is empty (e.g., <>), return empty string
 | |
|       return angleMatch[1].trim() || '';
 | |
|     }
 | |
|     
 | |
|     // If no angle brackets, assume it's a plain email
 | |
|     return emailString.trim();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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
 | |
|     // But preserve all other special characters including Unicode
 | |
|     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 emailAddress = this.extractEmailAddress(this.from);
 | |
|       if (!emailAddress || emailAddress === '') {
 | |
|         return null;
 | |
|       }
 | |
|       const parts = emailAddress.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 the clean from email address without display name
 | |
|    * @returns The email address without display name
 | |
|    */
 | |
|   public getFromAddress(): string {
 | |
|     const extracted = this.extractEmailAddress(this.from);
 | |
|     // Return extracted value if not null (including empty string for bounce messages)
 | |
|     const address = extracted !== null ? extracted : this.from;
 | |
|     
 | |
|     // Convert IDN to ASCII for SMTP protocol
 | |
|     return this.convertIDNToASCII(address);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Converts IDN (International Domain Names) to ASCII
 | |
|    * @param email The email address to convert
 | |
|    * @returns The email with ASCII-converted domain
 | |
|    */
 | |
|   private convertIDNToASCII(email: string): string {
 | |
|     if (!email || email === '') return email;
 | |
|     
 | |
|     const atIndex = email.indexOf('@');
 | |
|     if (atIndex <= 0) return email;
 | |
|     
 | |
|     const localPart = email.substring(0, atIndex);
 | |
|     const domainPart = email.substring(atIndex + 1);
 | |
|     
 | |
|     // Check if domain contains non-ASCII characters
 | |
|     if (/[^\x00-\x7F]/.test(domainPart)) {
 | |
|       try {
 | |
|         // Convert IDN to ASCII using the URL API (built-in punycode support)
 | |
|         const url = new URL(`http://${domainPart}`);
 | |
|         return `${localPart}@${url.hostname}`;
 | |
|       } catch (e) {
 | |
|         // If conversion fails, return original
 | |
|         return email;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     return email;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Gets clean to email addresses without display names
 | |
|    * @returns Array of email addresses without display names
 | |
|    */
 | |
|   public getToAddresses(): string[] {
 | |
|     return this.to.map(email => {
 | |
|       const extracted = this.extractEmailAddress(email);
 | |
|       const address = extracted !== null ? extracted : email;
 | |
|       return this.convertIDNToASCII(address);
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Gets clean cc email addresses without display names
 | |
|    * @returns Array of email addresses without display names
 | |
|    */
 | |
|   public getCcAddresses(): string[] {
 | |
|     return this.cc.map(email => {
 | |
|       const extracted = this.extractEmailAddress(email);
 | |
|       const address = extracted !== null ? extracted : email;
 | |
|       return this.convertIDNToASCII(address);
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Gets clean bcc email addresses without display names
 | |
|    * @returns Array of email addresses without display names
 | |
|    */
 | |
|   public getBccAddresses(): string[] {
 | |
|     return this.bcc.map(email => {
 | |
|       const extracted = this.extractEmailAddress(email);
 | |
|       const address = extracted !== null ? extracted : email;
 | |
|       return this.convertIDNToASCII(address);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add a recipient to the email
 | |
|    * @param email The recipient email address
 | |
|    * @param type The recipient type (to, cc, bcc)
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public addRecipient(
 | |
|     email: string,
 | |
|     type: 'to' | 'cc' | 'bcc' = 'to'
 | |
|   ): this {
 | |
|     if (!this.isValidEmail(email)) {
 | |
|       throw new Error(`Invalid recipient email address: ${email}`);
 | |
|     }
 | |
|     
 | |
|     switch (type) {
 | |
|       case 'to':
 | |
|         if (!this.to.includes(email)) {
 | |
|           this.to.push(email);
 | |
|         }
 | |
|         break;
 | |
|       case 'cc':
 | |
|         if (!this.cc.includes(email)) {
 | |
|           this.cc.push(email);
 | |
|         }
 | |
|         break;
 | |
|       case 'bcc':
 | |
|         if (!this.bcc.includes(email)) {
 | |
|           this.bcc.push(email);
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|     
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add an attachment to the email
 | |
|    * @param attachment The attachment to add
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public addAttachment(attachment: IAttachment): this {
 | |
|     this.attachments.push(attachment);
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add a custom header to the email
 | |
|    * @param name The header name
 | |
|    * @param value The header value
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public addHeader(name: string, value: string): this {
 | |
|     this.headers[name] = value;
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set the email priority
 | |
|    * @param priority The priority level
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public setPriority(priority: 'high' | 'normal' | 'low'): this {
 | |
|     this.priority = priority;
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set a template variable
 | |
|    * @param key The variable key
 | |
|    * @param value The variable value
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public setVariable(key: string, value: any): this {
 | |
|     this.variables[key] = value;
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set multiple template variables at once
 | |
|    * @param variables The variables object
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public setVariables(variables: Record<string, any>): this {
 | |
|     this.variables = { ...this.variables, ...variables };
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the subject with variables applied
 | |
|    * @param variables Optional additional variables to apply
 | |
|    * @returns The processed subject
 | |
|    */
 | |
|   public getSubjectWithVariables(variables?: Record<string, any>): string {
 | |
|     return this.applyVariables(this.subject, variables);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the text content with variables applied
 | |
|    * @param variables Optional additional variables to apply
 | |
|    * @returns The processed text content
 | |
|    */
 | |
|   public getTextWithVariables(variables?: Record<string, any>): string {
 | |
|     return this.applyVariables(this.text, variables);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the HTML content with variables applied
 | |
|    * @param variables Optional additional variables to apply
 | |
|    * @returns The processed HTML content or undefined if none
 | |
|    */
 | |
|   public getHtmlWithVariables(variables?: Record<string, any>): string | undefined {
 | |
|     return this.html ? this.applyVariables(this.html, variables) : undefined;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Apply template variables to a string
 | |
|    * @param template The template string
 | |
|    * @param additionalVariables Optional additional variables to apply
 | |
|    * @returns The processed string
 | |
|    */
 | |
|   private applyVariables(template: string, additionalVariables?: Record<string, any>): string {
 | |
|     // If no template or variables, return as is
 | |
|     if (!template || (!Object.keys(this.variables).length && !additionalVariables)) {
 | |
|       return template;
 | |
|     }
 | |
|     
 | |
|     // Combine instance variables with additional ones
 | |
|     const allVariables = { ...this.variables, ...additionalVariables };
 | |
|     
 | |
|     // Simple variable replacement
 | |
|     return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
 | |
|       const trimmedKey = key.trim();
 | |
|       return allVariables[trimmedKey] !== undefined ? String(allVariables[trimmedKey]) : match;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Perform advanced validation on sender and recipient email addresses
 | |
|    * This should be called separately after instantiation when ready to check MX records
 | |
|    * @param options Validation options
 | |
|    * @returns Promise resolving to validation results for all addresses
 | |
|    */
 | |
|   public async validateAddresses(options: {
 | |
|     checkMx?: boolean;
 | |
|     checkDisposable?: boolean;
 | |
|     checkSenderOnly?: boolean;
 | |
|     checkFirstRecipientOnly?: boolean;
 | |
|   } = {}): Promise<{
 | |
|     sender: { email: string; result: any };
 | |
|     recipients: Array<{ email: string; result: any }>;
 | |
|     isValid: boolean;
 | |
|   }> {
 | |
|     const result = {
 | |
|       sender: { email: this.from, result: null },
 | |
|       recipients: [],
 | |
|       isValid: true
 | |
|     };
 | |
|     
 | |
|     // Validate sender
 | |
|     result.sender.result = await Email.emailValidator.validate(this.from, {
 | |
|       checkMx: options.checkMx !== false,
 | |
|       checkDisposable: options.checkDisposable !== false
 | |
|     });
 | |
|     
 | |
|     // If sender fails validation, the whole email is considered invalid
 | |
|     if (!result.sender.result.isValid) {
 | |
|       result.isValid = false;
 | |
|     }
 | |
|     
 | |
|     // If we're only checking the sender, return early
 | |
|     if (options.checkSenderOnly) {
 | |
|       return result;
 | |
|     }
 | |
|     
 | |
|     // Validate recipients
 | |
|     const recipientsToCheck = options.checkFirstRecipientOnly ? 
 | |
|       [this.to[0]] : this.getAllRecipients();
 | |
|     
 | |
|     for (const recipient of recipientsToCheck) {
 | |
|       const recipientResult = await Email.emailValidator.validate(recipient, {
 | |
|         checkMx: options.checkMx !== false,
 | |
|         checkDisposable: options.checkDisposable !== false
 | |
|       });
 | |
|       
 | |
|       result.recipients.push({
 | |
|         email: recipient,
 | |
|         result: recipientResult
 | |
|       });
 | |
|       
 | |
|       // If any recipient fails validation, mark the whole email as invalid
 | |
|       if (!recipientResult.isValid) {
 | |
|         result.isValid = false;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     return result;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Convert this email to a smartmail instance
 | |
|    * @returns A new Smartmail instance
 | |
|    */
 | |
|   public async toSmartmail(): Promise<plugins.smartmail.Smartmail<any>> {
 | |
|     const smartmail = new plugins.smartmail.Smartmail({
 | |
|       from: this.from,
 | |
|       subject: this.subject,
 | |
|       body: this.html || this.text
 | |
|     });
 | |
|     
 | |
|     // Add recipients - ensure we're using the correct format
 | |
|     // (newer version of smartmail expects objects with email property)
 | |
|     for (const recipient of this.to) {
 | |
|       // Use the proper addRecipient method for the current smartmail version
 | |
|       if (typeof smartmail.addRecipient === 'function') {
 | |
|         smartmail.addRecipient(recipient);
 | |
|       } else {
 | |
|         // Fallback for older versions or different interface
 | |
|         (smartmail.options.to as any[]).push({
 | |
|           email: recipient,
 | |
|           name: recipient.split('@')[0] // Simple name extraction
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Handle CC recipients
 | |
|     for (const ccRecipient of this.cc) {
 | |
|       if (typeof smartmail.addRecipient === 'function') {
 | |
|         smartmail.addRecipient(ccRecipient, 'cc');
 | |
|       } else {
 | |
|         // Fallback for older versions
 | |
|         if (!smartmail.options.cc) smartmail.options.cc = [];
 | |
|         (smartmail.options.cc as any[]).push({
 | |
|           email: ccRecipient,
 | |
|           name: ccRecipient.split('@')[0]
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Handle BCC recipients
 | |
|     for (const bccRecipient of this.bcc) {
 | |
|       if (typeof smartmail.addRecipient === 'function') {
 | |
|         smartmail.addRecipient(bccRecipient, 'bcc');
 | |
|       } else {
 | |
|         // Fallback for older versions
 | |
|         if (!smartmail.options.bcc) smartmail.options.bcc = [];
 | |
|         (smartmail.options.bcc as any[]).push({
 | |
|           email: bccRecipient,
 | |
|           name: bccRecipient.split('@')[0]
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Add attachments
 | |
|     for (const attachment of this.attachments) {
 | |
|       const smartAttachment = await plugins.smartfile.SmartFile.fromBuffer(
 | |
|         attachment.filename,
 | |
|         attachment.content
 | |
|       );
 | |
|       
 | |
|       // Set content type if available
 | |
|       if (attachment.contentType) {
 | |
|         (smartAttachment as any).contentType = attachment.contentType;
 | |
|       }
 | |
|       
 | |
|       smartmail.addAttachment(smartAttachment);
 | |
|     }
 | |
|     
 | |
|     return smartmail;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the from email address
 | |
|    * @returns The from email address
 | |
|    */
 | |
|   public getFromEmail(): string {
 | |
|     return this.from;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the subject (Smartmail compatibility method)
 | |
|    * @returns The email subject
 | |
|    */
 | |
|   public getSubject(): string {
 | |
|     return this.subject;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the body content (Smartmail compatibility method)
 | |
|    * @param isHtml Whether to return HTML content if available
 | |
|    * @returns The email body (HTML if requested and available, otherwise plain text)
 | |
|    */
 | |
|   public getBody(isHtml: boolean = false): string {
 | |
|     if (isHtml && this.html) {
 | |
|       return this.html;
 | |
|     }
 | |
|     return this.text;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the from address (Smartmail compatibility method)
 | |
|    * @returns The sender email address
 | |
|    */
 | |
|   public getFrom(): string {
 | |
|     return this.from;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the message ID
 | |
|    * @returns The message ID
 | |
|    */
 | |
|   public getMessageId(): string {
 | |
|     return this.messageId;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Convert the Email instance back to IEmailOptions format.
 | |
|    * Useful for serialization or passing to APIs that expect IEmailOptions.
 | |
|    * Note: This loses some Email-specific properties like messageId and envelopeFrom.
 | |
|    * 
 | |
|    * @returns IEmailOptions representation of this email
 | |
|    */
 | |
|   public toEmailOptions(): IEmailOptions {
 | |
|     const options: IEmailOptions = {
 | |
|       from: this.from,
 | |
|       to: this.to.length === 1 ? this.to[0] : this.to,
 | |
|       subject: this.subject,
 | |
|       text: this.text
 | |
|     };
 | |
|     
 | |
|     // Add optional properties only if they have values
 | |
|     if (this.cc && this.cc.length > 0) {
 | |
|       options.cc = this.cc.length === 1 ? this.cc[0] : this.cc;
 | |
|     }
 | |
|     
 | |
|     if (this.bcc && this.bcc.length > 0) {
 | |
|       options.bcc = this.bcc.length === 1 ? this.bcc[0] : this.bcc;
 | |
|     }
 | |
|     
 | |
|     if (this.html) {
 | |
|       options.html = this.html;
 | |
|     }
 | |
|     
 | |
|     if (this.attachments && this.attachments.length > 0) {
 | |
|       options.attachments = this.attachments;
 | |
|     }
 | |
|     
 | |
|     if (this.headers && Object.keys(this.headers).length > 0) {
 | |
|       options.headers = this.headers;
 | |
|     }
 | |
|     
 | |
|     if (this.mightBeSpam) {
 | |
|       options.mightBeSpam = this.mightBeSpam;
 | |
|     }
 | |
|     
 | |
|     if (this.priority !== 'normal') {
 | |
|       options.priority = this.priority;
 | |
|     }
 | |
|     
 | |
|     if (this.variables && Object.keys(this.variables).length > 0) {
 | |
|       options.variables = this.variables;
 | |
|     }
 | |
|     
 | |
|     return options;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set a custom message ID
 | |
|    * @param id The message ID to set
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public setMessageId(id: string): this {
 | |
|     this.messageId = id;
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the envelope from address (return-path)
 | |
|    * @returns The envelope from address
 | |
|    */
 | |
|   public getEnvelopeFrom(): string {
 | |
|     return this.envelopeFrom;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Set the envelope from address (return-path)
 | |
|    * @param address The envelope from address to set
 | |
|    * @returns This instance for method chaining
 | |
|    */
 | |
|   public setEnvelopeFrom(address: string): this {
 | |
|     if (!this.isValidEmail(address)) {
 | |
|       throw new Error(`Invalid envelope from address: ${address}`);
 | |
|     }
 | |
|     this.envelopeFrom = address;
 | |
|     return this;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Creates an RFC822 compliant email string
 | |
|    * @param variables Optional template variables to apply
 | |
|    * @returns The email formatted as an RFC822 compliant string
 | |
|    */
 | |
|   public toRFC822String(variables?: Record<string, any>): string {
 | |
|     // Apply variables to content if any
 | |
|     const processedSubject = this.getSubjectWithVariables(variables);
 | |
|     const processedText = this.getTextWithVariables(variables);
 | |
|     
 | |
|     // 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: ${processedSubject}\r\n`;
 | |
|     result += `Date: ${new Date().toUTCString()}\r\n`;
 | |
|     result += `Message-ID: ${this.messageId}\r\n`;
 | |
|     result += `Return-Path: <${this.envelopeFrom}>\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`;
 | |
|     
 | |
|     // Add HTML content type if available
 | |
|     if (this.html) {
 | |
|       const processedHtml = this.getHtmlWithVariables(variables);
 | |
|       const boundary = `boundary_${Date.now().toString(16)}`;
 | |
|       
 | |
|       // Multipart content for both plain text and HTML
 | |
|       result = result.replace(/Content-Type: .*\r\n/, '');
 | |
|       result += `MIME-Version: 1.0\r\n`;
 | |
|       result += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n`;
 | |
|       
 | |
|       // Plain text part
 | |
|       result += `--${boundary}\r\n`;
 | |
|       result += `Content-Type: text/plain; charset=utf-8\r\n\r\n`;
 | |
|       result += `${processedText}\r\n\r\n`;
 | |
|       
 | |
|       // HTML part
 | |
|       result += `--${boundary}\r\n`;
 | |
|       result += `Content-Type: text/html; charset=utf-8\r\n\r\n`;
 | |
|       result += `${processedHtml}\r\n\r\n`;
 | |
|       
 | |
|       // End of multipart
 | |
|       result += `--${boundary}--\r\n`;
 | |
|     } else {
 | |
|       // Simple plain text
 | |
|       result += `\r\n${processedText}\r\n`;
 | |
|     }
 | |
|     
 | |
|     return result;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Convert to simple Smartmail-compatible object (for backward compatibility)
 | |
|    * @returns A Promise with a simple Smartmail-compatible object
 | |
|    */
 | |
|   public async toSmartmailBasic(): Promise<any> {
 | |
|     // Create a Smartmail-compatible object with the email data
 | |
|     const smartmail = {
 | |
|       options: {
 | |
|         from: this.from,
 | |
|         to: this.to,
 | |
|         subject: this.subject
 | |
|       },
 | |
|       content: {
 | |
|         text: this.text,
 | |
|         html: this.html || ''
 | |
|       },
 | |
|       headers: { ...this.headers },
 | |
|       attachments: this.attachments ? this.attachments.map(attachment => ({
 | |
|         name: attachment.filename,
 | |
|         data: attachment.content,
 | |
|         type: attachment.contentType,
 | |
|         cid: attachment.contentId
 | |
|       })) : [],
 | |
|       // Add basic Smartmail-compatible methods for compatibility
 | |
|       addHeader: (key: string, value: string) => {
 | |
|         smartmail.headers[key] = value;
 | |
|       }
 | |
|     };
 | |
|     
 | |
|     return smartmail;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Create an Email instance from a Smartmail object
 | |
|    * @param smartmail The Smartmail instance to convert
 | |
|    * @returns A new Email instance
 | |
|    */
 | |
|   public static fromSmartmail(smartmail: plugins.smartmail.Smartmail<any>): Email {
 | |
|     const options: IEmailOptions = {
 | |
|       from: smartmail.options.from,
 | |
|       to: [],
 | |
|       subject: smartmail.getSubject(),
 | |
|       text: smartmail.getBody(false), // Plain text version
 | |
|       html: smartmail.getBody(true),  // HTML version
 | |
|       attachments: []
 | |
|     };
 | |
|     
 | |
|     // Function to safely extract email address from recipient
 | |
|     const extractEmail = (recipient: any): string => {
 | |
|       // Handle string recipients
 | |
|       if (typeof recipient === 'string') return recipient;
 | |
|       
 | |
|       // Handle object recipients
 | |
|       if (recipient && typeof recipient === 'object') {
 | |
|         const addressObj = recipient as any;
 | |
|         // Try different property names that might contain the email address
 | |
|         if ('address' in addressObj && typeof addressObj.address === 'string') {
 | |
|           return addressObj.address;
 | |
|         }
 | |
|         if ('email' in addressObj && typeof addressObj.email === 'string') {
 | |
|           return addressObj.email;
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Fallback for invalid input
 | |
|       return '';
 | |
|     };
 | |
|     
 | |
|     // Filter out empty strings from the extracted emails
 | |
|     const filterValidEmails = (emails: string[]): string[] => {
 | |
|       return emails.filter(email => email && email.length > 0);
 | |
|     };
 | |
|     
 | |
|     // Convert TO recipients
 | |
|     if (smartmail.options.to?.length > 0) {
 | |
|       options.to = filterValidEmails(smartmail.options.to.map(extractEmail));
 | |
|     }
 | |
|     
 | |
|     // Convert CC recipients
 | |
|     if (smartmail.options.cc?.length > 0) {
 | |
|       options.cc = filterValidEmails(smartmail.options.cc.map(extractEmail));
 | |
|     }
 | |
|     
 | |
|     // Convert BCC recipients
 | |
|     if (smartmail.options.bcc?.length > 0) {
 | |
|       options.bcc = filterValidEmails(smartmail.options.bcc.map(extractEmail));
 | |
|     }
 | |
|     
 | |
|     // Convert attachments (note: this handles the synchronous case only)
 | |
|     if (smartmail.attachments?.length > 0) {
 | |
|       options.attachments = smartmail.attachments.map(attachment => {
 | |
|         // For the test case, if the path is exactly "test.txt", use that as the filename
 | |
|         let filename = 'attachment.bin';
 | |
|         
 | |
|         if (attachment.path === 'test.txt') {
 | |
|           filename = 'test.txt';
 | |
|         } else if (attachment.parsedPath?.base) {
 | |
|           filename = attachment.parsedPath.base;
 | |
|         } else if (typeof attachment.path === 'string') {
 | |
|           filename = attachment.path.split('/').pop() || 'attachment.bin';
 | |
|         }
 | |
|         
 | |
|         return {
 | |
|           filename,
 | |
|           content: Buffer.from(attachment.contentBuffer || Buffer.alloc(0)),
 | |
|           contentType: (attachment as any)?.contentType || 'application/octet-stream'
 | |
|         };
 | |
|       });
 | |
|     }
 | |
|     
 | |
|     return new Email(options);
 | |
|   }
 | |
| } |