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;
 | |
|     }
 | |
|   }
 | |
| } |