const nodemailer = require('nodemailer'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const { getFrontendBaseUrl } = require('../utils/frontendUrl'); 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}:${config.tls_reject_unauthorized}`; 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, tls: { // Allow ignoring SSL certificate errors when tls_reject_unauthorized is false rejectUnauthorized: config.tls_reject_unauthorized !== false } }); // 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; } } // Read the support contact email used in customer-facing notifications // (gallery_expired, archive_complete, …). Looks up `branding_support_email` // from app_settings (JSON-encoded), falling back to the SMTP from-address // so templates that reference {{support_email}} never render the literal // placeholder. Returns '' when neither is configured — templates should // degrade by hiding the line via a {{#if support_email}} block. async function getSupportEmail() { try { const row = await db('app_settings') .where('setting_key', 'branding_support_email') .first(); if (row && row.setting_value) { try { const parsed = JSON.parse(row.setting_value); if (typeof parsed === 'string' && parsed.trim()) return parsed.trim(); } catch (e) { if (typeof row.setting_value === 'string' && row.setting_value.trim()) { return row.setting_value.trim(); } } } } catch (err) { logger.debug('getSupportEmail: app_settings lookup failed', { error: err.message }); } try { const config = await db('email_configs').first(); if (config && config.from_email) return config.from_email; } catch (err) { logger.debug('getSupportEmail: email_configs lookup failed', { error: err.message }); } return ''; } // 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 a language if (email) { const domain = email.toLowerCase(); const domainLanguageMap = [ { domains: ['.de', '.at', '.ch', '.li'], language: 'de' }, { domains: ['.nl', '.be'], language: 'nl' }, { domains: ['.br', '.pt'], language: 'pt' }, { domains: ['.ru', '.su'], language: 'ru' }, ]; for (const { domains, language: lang } of domainLanguageMap) { if (domains.some(d => domain.endsWith(d))) { return lang; } } } return 'en'; // Default to English } // Darken a hex color by a percentage (0-1) function darkenColor(hex, amount = 0.15) { const num = parseInt(hex.replace('#', ''), 16); const r = Math.max(0, Math.min(255, ((num >> 16) & 0xFF) * (1 - amount))); const g = Math.max(0, Math.min(255, ((num >> 8) & 0xFF) * (1 - amount))); const b = Math.max(0, Math.min(255, (num & 0xFF) * (1 - amount))); return `#${(1 << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b)).toString(16).slice(1)}`; } // Wrap HTML body in the styled email template with header, footer, and logo async function wrapEmailHtml(htmlBody, subject, language = 'en') { // Email colour palette. The two original settings (email_primary_color and // email_secondary_color) keep their existing semantics so emails sent by // upgraded instances render byte-for-byte identically until an admin // touches the new fields. The six new tokens unlock full email theming // (body bg, container card, list panel, body text, muted text, button text) // and default to the previously hard-coded literals when absent. let logoUrl = ''; let companyName = 'PicPeak'; let primaryColor = '#5C8762'; let secondaryColor = '#f9f9f9'; let bodyBgColor = '#f5f5f5'; // outer wrapper + body background let containerBgColor = '#ffffff'; // email card let listBgColor = '#f9f9f9'; //