325 lines
9.8 KiB
TypeScript
325 lines
9.8 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import * as paths from '../../paths.js';
|
|
import { logger } from '../../logger.js';
|
|
|
|
/**
|
|
* 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 smartmail's capabilities
|
|
*/
|
|
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 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<T = any>(
|
|
templateId: string,
|
|
context?: ITemplateContext
|
|
): Promise<plugins.smartmail.Smartmail<T>> {
|
|
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<T>({
|
|
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<T = any>(
|
|
templateId: string,
|
|
context: ITemplateContext = {}
|
|
): Promise<plugins.smartmail.Smartmail<T>> {
|
|
const smartmail = await this.createSmartmail<T>(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<string> {
|
|
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<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('.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;
|
|
}
|
|
}
|
|
} |