import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { logger } from '../logger.js'; /** * Email template type definition */ export interface IEmailTemplate { 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 smartmail's capabilities */ export class TemplateManager { private templates: Map = 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: `

Welcome, {{firstName}}!

Thank you for joining {{serviceName}}. We're excited to have you on board.

To get started, visit your account.

`, 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: `

Password Reset Request

You recently requested to reset your password. Click the link below to reset it:

Reset Password

This link will expire in {{expiryHours}} hours.

If you didn't request a password reset, please ignore this email.

`, 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: `

{{title}}

{{message}}
`, 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(template: IEmailTemplate): 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(templateId: string): IEmailTemplate | undefined { return this.templates.get(templateId) as IEmailTemplate; } /** * 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 a Smartmail instance from a template * @param templateId The template ID * @param context The template context data * @returns A configured Smartmail instance */ public async createSmartmail( templateId: string, context?: ITemplateContext ): Promise> { const template = this.getTemplate(templateId); if (!template) { throw new Error(`Template with ID '${templateId}' not found`); } // Create Smartmail instance with template content const smartmail = new plugins.smartmail.Smartmail({ from: template.from || this.defaultConfig.from, subject: template.subject, body: template.bodyHtml || template.bodyText || '', creationObjectRef: context as T }); // Add any template attachments if (template.attachments && template.attachments.length > 0) { for (const attachment of template.attachments) { // Load attachment file try { const attachmentPath = plugins.path.isAbsolute(attachment.path) ? attachment.path : plugins.path.join(paths.MtaAttachmentsDir, attachment.path); // Use appropriate SmartFile method - either read from file or create with empty buffer // For a file path, use the fromFilePath static method const file = await plugins.smartfile.SmartFile.fromFilePath(attachmentPath); // Set content type if specified if (attachment.contentType) { (file as any).contentType = attachment.contentType; } smartmail.addAttachment(file); } catch (error) { logger.log('error', `Failed to add attachment '${attachment.name}': ${error.message}`); } } } // Apply template variables if context provided if (context) { // Use applyVariables from smartmail v2.1.0+ smartmail.applyVariables(context); } return smartmail; } /** * Create and completely process a Smartmail instance from a template * @param templateId The template ID * @param context The template context data * @returns A complete, processed Smartmail instance ready to send */ public async prepareEmail( templateId: string, context: ITemplateContext = {} ): Promise> { const smartmail = await this.createSmartmail(templateId, context); // Pre-compile all mustache templates (subject, body) smartmail.getSubject(); smartmail.getBody(); return smartmail; } /** * 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 { const smartmail = await this.prepareEmail(templateId, context); return smartmail.toMimeFormat(); } /** * Load templates from a directory * @param directory The directory containing template JSON files */ public async loadTemplatesFromDirectory(directory: string): Promise { 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('.json')); 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; } } }