freealberta/influence/app/services/emailTemplates.js

135 lines
4.7 KiB
JavaScript

const fs = require('fs').promises;
const path = require('path');
class EmailTemplateService {
constructor() {
this.templatesDir = path.join(__dirname, '../templates/email');
this.cache = new Map();
}
async loadTemplate(templateName, type = 'html') {
const cacheKey = `${templateName}.${type}`;
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const templatePath = path.join(this.templatesDir, `${templateName}.${type}`);
const template = await fs.readFile(templatePath, 'utf-8');
// Cache the template
this.cache.set(cacheKey, template);
return template;
} catch (error) {
console.error(`Failed to load email template ${templateName}.${type}:`, error);
throw new Error(`Email template not found: ${templateName}.${type}`);
}
}
processTemplate(template, variables) {
if (!template) return '';
let processed = template;
// Handle conditional blocks {{#if VARIABLE}}...{{/if}}
processed = processed.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, varName, content) => {
const value = variables[varName];
// Check if value exists and is not empty string
if (value !== undefined && value !== null && value !== '') {
// Recursively process the content inside the conditional block
return this.processTemplate(content, variables);
}
return '';
});
// Replace variables {{VARIABLE}}
processed = processed.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
const value = variables[varName];
// Return the value or empty string if undefined
return value !== undefined && value !== null ? String(value) : '';
});
// Handle line breaks in MESSAGE field for HTML templates
if (variables.MESSAGE && processed.includes('{{MESSAGE}}')) {
processed = processed.replace(/\{\{MESSAGE\}\}/g, variables.MESSAGE.replace(/\n/g, '<br>'));
}
return processed;
}
async render(templateName, variables) {
try {
// Load both HTML and text versions
const [htmlTemplate, textTemplate] = await Promise.all([
this.loadTemplate(templateName, 'html'),
this.loadTemplate(templateName, 'txt')
]);
// Add default variables
const defaultVariables = {
APP_NAME: process.env.APP_NAME || 'BNKops Influence Campaign',
TIMESTAMP: new Date().toLocaleString(),
...variables
};
// Use processTemplate which handles conditionals properly
return {
html: this.processTemplate(htmlTemplate, defaultVariables),
text: this.processTemplate(textTemplate, defaultVariables)
};
} catch (error) {
console.error('Failed to render email template:', error);
throw error;
}
}
// Get available template names
async getAvailableTemplates() {
try {
const files = await fs.readdir(this.templatesDir);
const templates = new Set();
files.forEach(file => {
const ext = path.extname(file);
const name = path.basename(file, ext);
if (ext === '.html' || ext === '.txt') {
templates.add(name);
}
});
return Array.from(templates);
} catch (error) {
console.error('Failed to get available templates:', error);
return [];
}
}
// Clear template cache (useful for development)
clearCache() {
this.cache.clear();
console.log('Email template cache cleared');
}
// Check if template exists
async templateExists(templateName) {
try {
const htmlPath = path.join(this.templatesDir, `${templateName}.html`);
const txtPath = path.join(this.templatesDir, `${templateName}.txt`);
// Check if at least one format exists
const [htmlExists, txtExists] = await Promise.all([
fs.access(htmlPath).then(() => true).catch(() => false),
fs.access(txtPath).then(() => true).catch(() => false)
]);
return htmlExists || txtExists;
} catch (error) {
return false;
}
}
}
module.exports = new EmailTemplateService();