320 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			320 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | import * as paths from '../../paths.ts'; | ||
|  | import { logger } from '../../logger.ts'; | ||
|  | import { Email, type IEmailOptions, type IAttachment } from './classes.email.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Email template type definition | ||
|  |  */ | ||
|  | export interface IEmailTemplate<T = any> { | ||
|  |   id: string; | ||
|  |   name: string; | ||
|  |   description: string; | ||
|  |   from: string; | ||
|  |   subject: string; | ||
|  |   bodyHtml: string; | ||
|  |   bodyText?: string; | ||
|  |   category?: string; | ||
|  |   sampleData?: T; | ||
|  |   attachments?: Array<{ | ||
|  |     name: string; | ||
|  |     path: string; | ||
|  |     contentType?: string; | ||
|  |   }>; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Email template context - data used to render the template | ||
|  |  */ | ||
|  | export interface ITemplateContext { | ||
|  |   [key: string]: any; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Template category definitions | ||
|  |  */ | ||
|  | export enum TemplateCategory { | ||
|  |   NOTIFICATION = 'notification', | ||
|  |   TRANSACTIONAL = 'transactional', | ||
|  |   MARKETING = 'marketing', | ||
|  |   SYSTEM = 'system' | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Enhanced template manager using Email class for template rendering | ||
|  |  */ | ||
|  | export class TemplateManager { | ||
|  |   private templates: Map<string, IEmailTemplate> = new Map(); | ||
|  |   private defaultConfig: { | ||
|  |     from: string; | ||
|  |     replyTo?: string; | ||
|  |     footerHtml?: string; | ||
|  |     footerText?: string; | ||
|  |   }; | ||
|  |    | ||
|  |   constructor(defaultConfig?: { | ||
|  |     from?: string; | ||
|  |     replyTo?: string; | ||
|  |     footerHtml?: string; | ||
|  |     footerText?: string; | ||
|  |   }) { | ||
|  |     // Set default configuration
 | ||
|  |     this.defaultConfig = { | ||
|  |       from: defaultConfig?.from || 'noreply@mail.lossless.com', | ||
|  |       replyTo: defaultConfig?.replyTo, | ||
|  |       footerHtml: defaultConfig?.footerHtml || '', | ||
|  |       footerText: defaultConfig?.footerText || '' | ||
|  |     }; | ||
|  |      | ||
|  |     // Initialize with built-in templates
 | ||
|  |     this.registerBuiltinTemplates(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Register built-in email templates | ||
|  |    */ | ||
|  |   private registerBuiltinTemplates(): void { | ||
|  |     // Welcome email
 | ||
|  |     this.registerTemplate<{ | ||
|  |       firstName: string; | ||
|  |       accountUrl: string; | ||
|  |     }>({ | ||
|  |       id: 'welcome', | ||
|  |       name: 'Welcome Email', | ||
|  |       description: 'Sent to users when they first sign up', | ||
|  |       from: this.defaultConfig.from, | ||
|  |       subject: 'Welcome to {{serviceName}}!', | ||
|  |       category: TemplateCategory.TRANSACTIONAL, | ||
|  |       bodyHtml: `
 | ||
|  |         <h1>Welcome, {{firstName}}!</h1> | ||
|  |         <p>Thank you for joining {{serviceName}}. We're excited to have you on board.</p> | ||
|  |         <p>To get started, <a href="{{accountUrl}}">visit your account</a>.</p> | ||
|  |       `,
 | ||
|  |       bodyText:  | ||
|  |         `Welcome, {{firstName}}!
 | ||
|  |          | ||
|  |         Thank you for joining {{serviceName}}. We're excited to have you on board. | ||
|  |          | ||
|  |         To get started, visit your account: {{accountUrl}} | ||
|  |         `,
 | ||
|  |       sampleData: { | ||
|  |         firstName: 'John', | ||
|  |         accountUrl: 'https://example.com/account' | ||
|  |       } | ||
|  |     }); | ||
|  |      | ||
|  |     // Password reset
 | ||
|  |     this.registerTemplate<{ | ||
|  |       resetUrl: string; | ||
|  |       expiryHours: number; | ||
|  |     }>({ | ||
|  |       id: 'password-reset', | ||
|  |       name: 'Password Reset', | ||
|  |       description: 'Sent when a user requests a password reset', | ||
|  |       from: this.defaultConfig.from, | ||
|  |       subject: 'Password Reset Request', | ||
|  |       category: TemplateCategory.TRANSACTIONAL, | ||
|  |       bodyHtml: `
 | ||
|  |         <h2>Password Reset Request</h2> | ||
|  |         <p>You recently requested to reset your password. Click the link below to reset it:</p> | ||
|  |         <p><a href="{{resetUrl}}">Reset Password</a></p> | ||
|  |         <p>This link will expire in {{expiryHours}} hours.</p> | ||
|  |         <p>If you didn't request a password reset, please ignore this email.</p> | ||
|  |       `,
 | ||
|  |       sampleData: { | ||
|  |         resetUrl: 'https://example.com/reset-password?token=abc123', | ||
|  |         expiryHours: 24 | ||
|  |       } | ||
|  |     }); | ||
|  |      | ||
|  |     // System notification
 | ||
|  |     this.registerTemplate({ | ||
|  |       id: 'system-notification', | ||
|  |       name: 'System Notification', | ||
|  |       description: 'General system notification template', | ||
|  |       from: this.defaultConfig.from, | ||
|  |       subject: '{{subject}}', | ||
|  |       category: TemplateCategory.SYSTEM, | ||
|  |       bodyHtml: `
 | ||
|  |         <h2>{{title}}</h2> | ||
|  |         <div>{{message}}</div> | ||
|  |       `,
 | ||
|  |       sampleData: { | ||
|  |         subject: 'Important System Notification', | ||
|  |         title: 'System Maintenance', | ||
|  |         message: 'The system will be undergoing maintenance on Saturday from 2-4am UTC.' | ||
|  |       } | ||
|  |     }); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Register a new email template | ||
|  |    * @param template The email template to register | ||
|  |    */ | ||
|  |   public registerTemplate<T = any>(template: IEmailTemplate<T>): void { | ||
|  |     if (this.templates.has(template.id)) { | ||
|  |       logger.log('warn', `Template with ID '${template.id}' already exists and will be overwritten`); | ||
|  |     } | ||
|  |      | ||
|  |     // Add footer to templates if configured
 | ||
|  |     if (this.defaultConfig.footerHtml && template.bodyHtml) { | ||
|  |       template.bodyHtml += this.defaultConfig.footerHtml; | ||
|  |     } | ||
|  |      | ||
|  |     if (this.defaultConfig.footerText && template.bodyText) { | ||
|  |       template.bodyText += this.defaultConfig.footerText; | ||
|  |     } | ||
|  |      | ||
|  |     this.templates.set(template.id, template); | ||
|  |     logger.log('info', `Registered email template: ${template.id}`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get an email template by ID | ||
|  |    * @param templateId The template ID | ||
|  |    * @returns The template or undefined if not found | ||
|  |    */ | ||
|  |   public getTemplate<T = any>(templateId: string): IEmailTemplate<T> | undefined { | ||
|  |     return this.templates.get(templateId) as IEmailTemplate<T>; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * List all available templates | ||
|  |    * @param category Optional category filter | ||
|  |    * @returns Array of email templates | ||
|  |    */ | ||
|  |   public listTemplates(category?: TemplateCategory): IEmailTemplate[] { | ||
|  |     const templates = Array.from(this.templates.values()); | ||
|  |     if (category) { | ||
|  |       return templates.filter(template => template.category === category); | ||
|  |     } | ||
|  |     return templates; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Create an Email instance from a template | ||
|  |    * @param templateId The template ID | ||
|  |    * @param context The template context data | ||
|  |    * @returns A configured Email instance | ||
|  |    */ | ||
|  |   public async createEmail<T = any>( | ||
|  |     templateId: string, | ||
|  |     context?: ITemplateContext | ||
|  |   ): Promise<Email> { | ||
|  |     const template = this.getTemplate(templateId); | ||
|  |      | ||
|  |     if (!template) { | ||
|  |       throw new Error(`Template with ID '${templateId}' not found`); | ||
|  |     } | ||
|  |      | ||
|  |     // Build attachments array for Email
 | ||
|  |     const attachments: IAttachment[] = []; | ||
|  |      | ||
|  |     if (template.attachments && template.attachments.length > 0) { | ||
|  |       for (const attachment of template.attachments) { | ||
|  |         try { | ||
|  |           const attachmentPath = plugins.path.isAbsolute(attachment.path)  | ||
|  |             ? attachment.path  | ||
|  |             : plugins.path.join(paths.MtaAttachmentsDir, attachment.path); | ||
|  |              | ||
|  |           // Read the file
 | ||
|  |           const fileBuffer = await plugins.fs.promises.readFile(attachmentPath); | ||
|  |            | ||
|  |           attachments.push({ | ||
|  |             filename: attachment.name, | ||
|  |             content: fileBuffer, | ||
|  |             contentType: attachment.contentType || 'application/octet-stream' | ||
|  |           }); | ||
|  |         } catch (error) { | ||
|  |           logger.log('error', `Failed to add attachment '${attachment.name}': ${error.message}`); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Create Email instance with template content
 | ||
|  |     const emailOptions: IEmailOptions = { | ||
|  |       from: template.from || this.defaultConfig.from, | ||
|  |       subject: template.subject, | ||
|  |       text: template.bodyText || '', | ||
|  |       html: template.bodyHtml, | ||
|  |       // Note: 'to' is intentionally omitted for templates
 | ||
|  |       attachments, | ||
|  |       variables: context || {} | ||
|  |     }; | ||
|  |      | ||
|  |     return new Email(emailOptions); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Create and completely process an Email instance from a template | ||
|  |    * @param templateId The template ID | ||
|  |    * @param context The template context data | ||
|  |    * @returns A complete, processed Email instance ready to send | ||
|  |    */ | ||
|  |   public async prepareEmail<T = any>( | ||
|  |     templateId: string, | ||
|  |     context: ITemplateContext = {} | ||
|  |   ): Promise<Email> { | ||
|  |     const email = await this.createEmail<T>(templateId, context); | ||
|  |      | ||
|  |     // Email class processes variables when needed, no pre-compilation required
 | ||
|  |      | ||
|  |     return email; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Create a MIME-formatted email from a template | ||
|  |    * @param templateId The template ID | ||
|  |    * @param context The template context data | ||
|  |    * @returns A MIME-formatted email string | ||
|  |    */ | ||
|  |   public async createMimeEmail( | ||
|  |     templateId: string, | ||
|  |     context: ITemplateContext = {} | ||
|  |   ): Promise<string> { | ||
|  |     const email = await this.prepareEmail(templateId, context); | ||
|  |     return email.toRFC822String(context); | ||
|  |   } | ||
|  |    | ||
|  |    | ||
|  |   /** | ||
|  |    * Load templates from a directory | ||
|  |    * @param directory The directory containing template JSON files | ||
|  |    */ | ||
|  |   public async loadTemplatesFromDirectory(directory: string): Promise<void> { | ||
|  |     try { | ||
|  |       // Ensure directory exists
 | ||
|  |       if (!plugins.fs.existsSync(directory)) { | ||
|  |         logger.log('error', `Template directory does not exist: ${directory}`); | ||
|  |         return; | ||
|  |       } | ||
|  |        | ||
|  |       // Get all JSON files
 | ||
|  |       const files = plugins.fs.readdirSync(directory) | ||
|  |         .filter(file => file.endsWith('.tson')); | ||
|  |        | ||
|  |       for (const file of files) { | ||
|  |         try { | ||
|  |           const filePath = plugins.path.join(directory, file); | ||
|  |           const content = plugins.fs.readFileSync(filePath, 'utf8'); | ||
|  |           const template = JSON.parse(content) as IEmailTemplate; | ||
|  |            | ||
|  |           // Validate template
 | ||
|  |           if (!template.id || !template.subject || (!template.bodyHtml && !template.bodyText)) { | ||
|  |             logger.log('warn', `Invalid template in ${file}: missing required fields`); | ||
|  |             continue; | ||
|  |           } | ||
|  |            | ||
|  |           this.registerTemplate(template); | ||
|  |         } catch (error) { | ||
|  |           logger.log('error', `Error loading template from ${file}: ${error.message}`); | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       logger.log('info', `Loaded ${this.templates.size} email templates`); | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Failed to load templates from directory: ${error.message}`); | ||
|  |       throw error; | ||
|  |     } | ||
|  |   } | ||
|  | } |