26c05912fc
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m21s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s
- Fix database connection error "getaddrinfo ENOTFOUND postgres" - Add wait-for-db.sh script to ensure PostgreSQL is ready before starting - Fix email processor initialization timing issue - Add missing storage path environment variables - Add database dependency to backend service - Enhance health check endpoint with database connectivity check - Update production database defaults to match docker-compose - Install postgresql-client in Docker image for health checks - Document all required environment variables in .env.example Fixes immediate production deployment failures and ensures proper service startup order. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
412 lines
11 KiB
JavaScript
412 lines
11 KiB
JavaScript
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
|
|
async 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 || '';
|
|
|
|
// 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`;
|
|
|
|
// Process welcome message section if present
|
|
let welcomeMessageSection = '';
|
|
if (variables.welcome_message && variables.welcome_message.trim() !== '') {
|
|
const welcomeTitle = language === 'de' ? 'Persönliche Nachricht:' : 'Personal Message:';
|
|
welcomeMessageSection = `
|
|
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
|
<p style="margin: 0 0 10px 0; font-weight: 600; color: #374151;">${welcomeTitle}</p>
|
|
<p style="margin: 0; color: #4b5563;">${variables.welcome_message}</p>
|
|
</div>`;
|
|
}
|
|
|
|
// 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 || '');
|
|
});
|
|
|
|
// Replace welcome message section placeholder
|
|
htmlBody = htmlBody.replace(/{{welcome_message_section}}/g, welcomeMessageSection);
|
|
|
|
// Wrap HTML body in styled template
|
|
const styledHtmlBody = `
|
|
<!DOCTYPE html>
|
|
<html lang="${language}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${subject}</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
.email-wrapper {
|
|
background-color: #f5f5f5;
|
|
padding: 40px 20px;
|
|
}
|
|
.email-container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
background-color: #ffffff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
}
|
|
.email-header {
|
|
background-color: #5C8762;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
.logo {
|
|
max-width: 180px;
|
|
height: auto;
|
|
margin-bottom: 10px;
|
|
}
|
|
.email-content {
|
|
padding: 40px 30px;
|
|
}
|
|
.email-content h2 {
|
|
color: #5C8762;
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
font-size: 24px;
|
|
}
|
|
.email-content p {
|
|
line-height: 1.6;
|
|
margin-bottom: 15px;
|
|
}
|
|
.email-content ul {
|
|
background-color: #f9f9f9;
|
|
padding: 20px 20px 20px 40px;
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
}
|
|
.email-content li {
|
|
margin-bottom: 10px;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
padding: 12px 30px;
|
|
background-color: #5C8762;
|
|
color: white !important;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
font-weight: 500;
|
|
margin: 20px 0;
|
|
}
|
|
.button:hover {
|
|
background-color: #4a6f4f;
|
|
}
|
|
.email-footer {
|
|
background-color: #f9f9f9;
|
|
padding: 30px;
|
|
text-align: center;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
.email-footer img {
|
|
max-width: 120px;
|
|
height: auto;
|
|
margin-bottom: 15px;
|
|
opacity: 0.8;
|
|
}
|
|
.email-footer p {
|
|
color: #666;
|
|
font-size: 14px;
|
|
margin: 5px 0;
|
|
}
|
|
a {
|
|
color: #5C8762;
|
|
text-decoration: underline;
|
|
}
|
|
a:hover {
|
|
color: #4a6f4f;
|
|
}
|
|
strong {
|
|
color: #333;
|
|
}
|
|
@media only screen and (max-width: 600px) {
|
|
.email-wrapper {
|
|
padding: 20px 10px;
|
|
}
|
|
.email-content {
|
|
padding: 30px 20px;
|
|
}
|
|
.email-header {
|
|
padding: 20px;
|
|
}
|
|
.logo {
|
|
max-width: 150px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="email-wrapper">
|
|
<div class="email-container">
|
|
<div class="email-header">
|
|
<img src="${logoFullUrl}" alt="${companyName}" class="logo">
|
|
</div>
|
|
<div class="email-content">
|
|
${htmlBody}
|
|
</div>
|
|
<div class="email-footer">
|
|
<img src="${logoFullUrl}" alt="${companyName}">
|
|
<p>${companyName}</p>
|
|
<p style="font-size: 12px; color: #999;">© ${new Date().getFullYear()} ${companyName}. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
|
|
return { subject, htmlBody: styledHtmlBody, 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 } = await 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 - DISABLED for production startup
|
|
// This will be called from server.js after database is ready
|
|
// initializeTransporter().then(() => {
|
|
// startEmailQueueProcessor();
|
|
// });
|
|
|
|
module.exports = {
|
|
initializeTransporter,
|
|
startEmailQueueProcessor,
|
|
sendTemplateEmail,
|
|
processEmailQueue,
|
|
queueEmail,
|
|
startEmailQueueProcessor,
|
|
stopEmailQueueProcessor
|
|
}; |