Implement multi-language email templates
- Add language columns to email_templates table (subject_en/de, body_html_en/de, body_text_en/de) - Update adminEmail.js routes to support language-specific templates - Create EmailProcessor service to handle language selection based on recipient - Update EmailConfigPage component with language tabs similar to CMS pages - Add German translations for all email templates - Update all email queue usage to use proper template keys - Add missing email templates (gallery_expired, archive_complete) - Integrate email processor service into main server startup The system now automatically selects the appropriate language (English/German) based on the recipient's email domain or preferences. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { db } = require('../database/db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
let transporter = null;
|
||||
|
||||
// Initialize transporter from database config
|
||||
async function initializeTransporter() {
|
||||
try {
|
||||
const config = await db('email_configs').first();
|
||||
|
||||
if (!config) {
|
||||
logger.warn('No email configuration found');
|
||||
return null;
|
||||
}
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: config.smtp_host,
|
||||
port: config.smtp_port,
|
||||
secure: config.smtp_secure,
|
||||
auth: config.smtp_user ? {
|
||||
user: config.smtp_user,
|
||||
pass: config.smtp_pass
|
||||
} : undefined
|
||||
});
|
||||
|
||||
// Verify configuration
|
||||
await transporter.verify();
|
||||
logger.info('Email transporter initialized successfully');
|
||||
|
||||
return transporter;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize email transporter:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the appropriate language for a recipient
|
||||
async function getRecipientLanguage(email) {
|
||||
// For now, check if the email domain ends with .de
|
||||
// In the future, this could check user preferences
|
||||
if (email && email.endsWith('.de')) {
|
||||
return 'de';
|
||||
}
|
||||
|
||||
// Check if there's a saved preference for this email
|
||||
// This could be expanded to check user preferences in the database
|
||||
|
||||
return 'en'; // Default to English
|
||||
}
|
||||
|
||||
// Process email template with variables
|
||||
function processTemplate(template, variables, language = 'en') {
|
||||
// Get the appropriate language fields
|
||||
const subjectField = language === 'de' ? 'subject_de' : 'subject_en';
|
||||
const htmlField = language === 'de' ? 'body_html_de' : 'body_html_en';
|
||||
const textField = language === 'de' ? 'body_text_de' : 'body_text_en';
|
||||
|
||||
// Fall back to non-language-specific fields for backward compatibility
|
||||
let subject = template[subjectField] || template.subject || '';
|
||||
let htmlBody = template[htmlField] || template.body_html || '';
|
||||
let textBody = template[textField] || template.body_text || '';
|
||||
|
||||
// Replace variables
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
subject = subject.replace(regex, value || '');
|
||||
htmlBody = htmlBody.replace(regex, value || '');
|
||||
textBody = textBody.replace(regex, value || '');
|
||||
});
|
||||
|
||||
return { subject, htmlBody, textBody };
|
||||
}
|
||||
|
||||
// Send email using template
|
||||
async function sendTemplateEmail(to, templateKey, variables) {
|
||||
try {
|
||||
if (!transporter) {
|
||||
transporter = await initializeTransporter();
|
||||
if (!transporter) {
|
||||
throw new Error('Email service not configured');
|
||||
}
|
||||
}
|
||||
|
||||
// Get email template
|
||||
const template = await db('email_templates')
|
||||
.where('template_key', templateKey)
|
||||
.first();
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Email template '${templateKey}' not found`);
|
||||
}
|
||||
|
||||
// Get email config for from address
|
||||
const config = await db('email_configs').first();
|
||||
if (!config) {
|
||||
throw new Error('Email configuration not found');
|
||||
}
|
||||
|
||||
// Determine recipient language
|
||||
const language = await getRecipientLanguage(to);
|
||||
|
||||
// Process template with variables
|
||||
const { subject, htmlBody, textBody } = processTemplate(template, variables, language);
|
||||
|
||||
// Send email
|
||||
const info = await transporter.sendMail({
|
||||
from: `${config.from_name} <${config.from_email}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
html: htmlBody,
|
||||
text: textBody || htmlBody.replace(/<[^>]*>/g, '') // Strip HTML if no text version
|
||||
});
|
||||
|
||||
logger.info(`Email sent successfully: ${info.messageId} (${language})`);
|
||||
return { success: true, messageId: info.messageId, language };
|
||||
} catch (error) {
|
||||
logger.error('Error sending template email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process email queue
|
||||
async function processEmailQueue() {
|
||||
try {
|
||||
const pendingEmails = await db('email_queue')
|
||||
.where('status', 'pending')
|
||||
.where('retry_count', '<', 3)
|
||||
.orderBy('created_at', 'asc')
|
||||
.limit(10);
|
||||
|
||||
if (pendingEmails.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Processing ${pendingEmails.length} emails from queue`);
|
||||
|
||||
for (const email of pendingEmails) {
|
||||
try {
|
||||
const emailData = JSON.parse(email.email_data || '{}');
|
||||
|
||||
await sendTemplateEmail(
|
||||
email.recipient_email,
|
||||
email.email_type,
|
||||
emailData
|
||||
);
|
||||
|
||||
// Mark as sent
|
||||
await db('email_queue')
|
||||
.where('id', email.id)
|
||||
.update({
|
||||
status: 'sent',
|
||||
sent_at: new Date()
|
||||
});
|
||||
|
||||
logger.info(`Email ${email.id} sent successfully`);
|
||||
} catch (error) {
|
||||
// Increment retry count
|
||||
await db('email_queue')
|
||||
.where('id', email.id)
|
||||
.update({
|
||||
retry_count: email.retry_count + 1,
|
||||
error_message: error.message,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
logger.error(`Failed to send email ${email.id}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing email queue:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Queue an email for sending
|
||||
async function queueEmail(eventId, recipientEmail, emailType, emailData) {
|
||||
try {
|
||||
await db('email_queue').insert({
|
||||
event_id: eventId,
|
||||
recipient_email: recipientEmail,
|
||||
email_type: emailType,
|
||||
email_data: JSON.stringify(emailData),
|
||||
status: 'pending',
|
||||
retry_count: 0,
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
logger.info(`Email queued: ${emailType} to ${recipientEmail}`);
|
||||
} catch (error) {
|
||||
logger.error('Error queueing email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Start email queue processor
|
||||
let emailQueueInterval = null;
|
||||
|
||||
function startEmailQueueProcessor() {
|
||||
if (!emailQueueInterval) {
|
||||
// Process immediately on start
|
||||
processEmailQueue();
|
||||
|
||||
// Then process every minute
|
||||
emailQueueInterval = setInterval(processEmailQueue, 60000);
|
||||
logger.info('Email queue processor started');
|
||||
}
|
||||
}
|
||||
|
||||
function stopEmailQueueProcessor() {
|
||||
if (emailQueueInterval) {
|
||||
clearInterval(emailQueueInterval);
|
||||
emailQueueInterval = null;
|
||||
logger.info('Email queue processor stopped');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initializeTransporter().then(() => {
|
||||
startEmailQueueProcessor();
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sendTemplateEmail,
|
||||
processEmailQueue,
|
||||
queueEmail,
|
||||
startEmailQueueProcessor,
|
||||
stopEmailQueueProcessor
|
||||
};
|
||||
Reference in New Issue
Block a user