const nodemailer = require('nodemailer'); const Handlebars = require('handlebars'); const { db } = require('../database/db'); const logger = require('../utils/logger'); let transporter = null; let lastConfigHash = null; // Generate hash from config for change detection function generateConfigHash(config) { const crypto = require('crypto'); const configString = `${config.smtp_host}:${config.smtp_port}:${config.smtp_user}:${config.smtp_pass}:${config.smtp_secure}`; return crypto.createHash('md5').update(configString).digest('hex'); } // Initialize transporter from database config async function initializeTransporter(forceReinit = false) { try { const config = await db('email_configs').first(); if (!config) { logger.warn('No email configuration found'); return null; } // Check if configuration has changed const currentConfigHash = generateConfigHash(config); if (!forceReinit && transporter && currentConfigHash === lastConfigHash) { // Configuration hasn't changed, return existing transporter return transporter; } // Configuration has changed or first initialization logger.info('Initializing email transporter' + (lastConfigHash && currentConfigHash !== lastConfigHash ? ' (configuration changed)' : '')); 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'); // Update the config hash lastConfigHash = currentConfigHash; return transporter; } catch (error) { logger.error('Failed to initialize email transporter:', error); transporter = null; lastConfigHash = null; return null; } } // Get the appropriate language for a recipient async function getRecipientLanguage(email, eventId = null) { // First priority: Check event language setting if eventId is provided if (eventId) { try { const event = await db('events').where('id', eventId).first(); if (event && event.language) { return event.language; } } catch (error) { logger.error('Error fetching event language:', error); } } // Second priority: Check app_settings for general default language try { const langSetting = await db('app_settings') .where('setting_key', 'general_default_language') .first(); if (langSetting && langSetting.setting_value) { return langSetting.setting_value; } } catch (error) { logger.error('Error fetching app settings language:', error); } // Third priority: Check email configs for default language try { const emailConfig = await db('email_configs').first(); if (emailConfig && emailConfig.default_language) { return emailConfig.default_language; } } catch (error) { logger.error('Error fetching email config language:', error); } // Fourth priority: Check if the email domain suggests German if (email) { const germanDomains = ['.de', '.at', '.ch', '.li']; const domain = email.toLowerCase(); if (germanDomains.some(d => domain.endsWith(d))) { return 'de'; } } return 'en'; // Default to English } // Process email template with variables async function processTemplate(template, variables, language = 'en') { // Import date formatter and text formatters const { formatDate } = require('../utils/dateFormatter'); const { formatWelcomeMessage } = require('../utils/formatters'); // 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 || ''; // Process variables before template compilation const processedVariables = { ...variables }; // Handle password security message if (processedVariables.gallery_password === '{{password_security_message}}') { processedVariables.gallery_password = language === 'de' ? '(Aus Sicherheitsgründen nicht angezeigt)' : '(Not shown for security reasons)'; } // Format dates if they exist if (processedVariables.event_date) { processedVariables.event_date = await formatDate(processedVariables.event_date, language); } if (processedVariables.expiry_date) { processedVariables.expiry_date = await formatDate(processedVariables.expiry_date, language); } if (processedVariables.archive_date) { processedVariables.archive_date = await formatDate(processedVariables.archive_date, language); } // Format welcome message for HTML display (preserve line breaks) if (processedVariables.welcome_message) { processedVariables.welcome_message = formatWelcomeMessage(processedVariables.welcome_message); } // Get branding settings for logo let logoUrl = ''; let companyName = 'PicPeak'; try { const brandingSettings = await db('app_settings') .whereIn('setting_key', ['branding_logo_url', 'branding_company_name']) .select('setting_key', 'setting_value'); brandingSettings.forEach(setting => { if (setting.setting_key === 'branding_logo_url' && setting.setting_value) { try { logoUrl = JSON.parse(setting.setting_value); } catch (e) { logoUrl = setting.setting_value; } } else if (setting.setting_key === 'branding_company_name' && setting.setting_value) { try { companyName = JSON.parse(setting.setting_value); } catch (e) { companyName = setting.setting_value; } } }); } catch (error) { logger.error('Error fetching branding settings:', error); } // If no custom logo, use default PicPeak logo const apiUrl = process.env.API_URL || 'http://localhost:3001'; const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3005'; const logoFullUrl = logoUrl ? `${apiUrl}${logoUrl}` : `${frontendUrl}/picpeak-logo-transparent.png`; // Compile templates with Handlebars const subjectTemplate = Handlebars.compile(subject); const htmlTemplate = Handlebars.compile(htmlBody); const textTemplate = Handlebars.compile(textBody); // Process templates with processedVariables (includes formatted dates and security messages) subject = subjectTemplate(processedVariables); htmlBody = htmlTemplate(processedVariables); textBody = textTemplate(processedVariables); // Wrap HTML body in styled template const styledHtmlBody = `