- Introduced a new API endpoint for sending email notifications using Nodemailer, allowing for customizable email content and recipient management. - Enhanced the notification configuration to support various recipient types (email, user, role, variable) and added error handling for recipient resolution. - Updated the process builder to include HTML message formatting and improved logging for notification processing. - Modified the Nuxt configuration to include Nodemailer as a dependency and adjusted security settings for the notifications API. - Refactored process definition JSON to accommodate changes in notification handling and updated UI components accordingly.
452 lines
13 KiB
JavaScript
452 lines
13 KiB
JavaScript
import nodemailer from 'nodemailer';
|
||
import { PrismaClient } from '@prisma/client';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// Email templates for different notification types
|
||
const emailTemplates = {
|
||
info: {
|
||
color: '#3b82f6',
|
||
backgroundColor: '#eff6ff',
|
||
icon: 'ℹ️'
|
||
},
|
||
success: {
|
||
color: '#10b981',
|
||
backgroundColor: '#ecfdf5',
|
||
icon: '✅'
|
||
},
|
||
warning: {
|
||
color: '#f59e0b',
|
||
backgroundColor: '#fffbeb',
|
||
icon: '⚠️'
|
||
},
|
||
error: {
|
||
color: '#ef4444',
|
||
backgroundColor: '#fef2f2',
|
||
icon: '❌'
|
||
},
|
||
reminder: {
|
||
color: '#8b5cf6',
|
||
backgroundColor: '#f3e8ff',
|
||
icon: '⏰'
|
||
},
|
||
status: {
|
||
color: '#6366f1',
|
||
backgroundColor: '#eef2ff',
|
||
icon: '📊'
|
||
}
|
||
};
|
||
|
||
// Create Nodemailer transporter with configurable email provider settings
|
||
function createTransporter() {
|
||
// Default configuration for common providers
|
||
const providerConfigs = {
|
||
gmail: {
|
||
host: 'smtp.gmail.com',
|
||
port: 587,
|
||
secure: false
|
||
},
|
||
outlook: {
|
||
host: 'smtp-mail.outlook.com',
|
||
port: 587,
|
||
secure: false
|
||
},
|
||
yahoo: {
|
||
host: 'smtp.mail.yahoo.com',
|
||
port: 587,
|
||
secure: false
|
||
},
|
||
proton: {
|
||
host: 'mail.protonmail.ch',
|
||
port: 587,
|
||
secure: false,
|
||
tls: {
|
||
ciphers: 'SSLv3',
|
||
rejectUnauthorized: false
|
||
}
|
||
}
|
||
};
|
||
|
||
// Get provider from environment or default to custom
|
||
const provider = process.env.EMAIL_PROVIDER?.toLowerCase() || 'custom';
|
||
|
||
// Use provider-specific config or custom config
|
||
let config;
|
||
if (providerConfigs[provider]) {
|
||
config = {
|
||
...providerConfigs[provider],
|
||
auth: {
|
||
user: process.env.EMAIL_USER,
|
||
pass: process.env.EMAIL_PASSWORD
|
||
}
|
||
};
|
||
} else {
|
||
// Custom SMTP configuration
|
||
config = {
|
||
host: process.env.SMTP_HOST,
|
||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||
secure: process.env.SMTP_SECURE === 'true', // true for SSL (port 465), false for STARTTLS (port 587)
|
||
auth: {
|
||
user: process.env.EMAIL_USER,
|
||
pass: process.env.EMAIL_PASSWORD
|
||
}
|
||
};
|
||
|
||
// Add custom TLS settings if provided
|
||
if (process.env.SMTP_TLS_CIPHERS || process.env.SMTP_TLS_REJECT_UNAUTHORIZED) {
|
||
config.tls = {};
|
||
if (process.env.SMTP_TLS_CIPHERS) {
|
||
config.tls.ciphers = process.env.SMTP_TLS_CIPHERS;
|
||
}
|
||
if (process.env.SMTP_TLS_REJECT_UNAUTHORIZED) {
|
||
config.tls.rejectUnauthorized = process.env.SMTP_TLS_REJECT_UNAUTHORIZED === 'true';
|
||
}
|
||
}
|
||
}
|
||
|
||
return nodemailer.createTransport(config);
|
||
}
|
||
|
||
// Generate HTML email template
|
||
function generateEmailTemplate(notificationType, subject, message, processName) {
|
||
const template = emailTemplates[notificationType] || emailTemplates.info;
|
||
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>${subject}</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
|
||
.container { max-width: 600px; margin: 0 auto; background-color: white; }
|
||
.header { background-color: ${template.backgroundColor}; padding: 20px; border-bottom: 3px solid ${template.color}; }
|
||
.header h1 { color: ${template.color}; margin: 0; font-size: 24px; }
|
||
.icon { font-size: 32px; margin-bottom: 10px; }
|
||
.content { padding: 30px; }
|
||
.message { font-size: 16px; line-height: 1.6; color: #333; margin-bottom: 20px; }
|
||
.footer { background-color: #f8f9fa; padding: 20px; text-align: center; font-size: 12px; color: #6c757d; border-top: 1px solid #e9ecef; }
|
||
.process-info { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid ${template.color}; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="icon">${template.icon}</div>
|
||
<h1>${subject}</h1>
|
||
</div>
|
||
<div class="content">
|
||
<div class="message">
|
||
${message.replace(/\n/g, '<br>')}
|
||
</div>
|
||
${processName ? `
|
||
<div class="process-info">
|
||
<strong>Process:</strong> ${processName}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
<div class="footer">
|
||
<p>This is an automated notification from the Corrad Process Management System.</p>
|
||
<p>Please do not reply to this email.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
}
|
||
|
||
// Resolve recipients based on recipient type
|
||
async function resolveRecipients(recipientType, recipientData, processVariables) {
|
||
const recipients = [];
|
||
|
||
try {
|
||
switch (recipientType) {
|
||
case 'email':
|
||
// Direct email address
|
||
if (recipientData.recipientEmail && isValidEmail(recipientData.recipientEmail)) {
|
||
recipients.push({
|
||
email: recipientData.recipientEmail,
|
||
name: recipientData.recipientEmail
|
||
});
|
||
}
|
||
break;
|
||
|
||
case 'user':
|
||
// Look up user from database
|
||
if (recipientData.recipientUser) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: recipientData.recipientUser },
|
||
select: { email: true, name: true, firstname: true, lastname: true }
|
||
});
|
||
|
||
if (user && user.email && isValidEmail(user.email)) {
|
||
const displayName = user.name || `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.email;
|
||
recipients.push({
|
||
email: user.email,
|
||
name: displayName
|
||
});
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'role':
|
||
// Look up users by role
|
||
if (recipientData.recipientRole) {
|
||
const usersWithRole = await prisma.user.findMany({
|
||
where: {
|
||
userrole: {
|
||
some: {
|
||
roleid: recipientData.recipientRole
|
||
}
|
||
}
|
||
},
|
||
select: { email: true, name: true, firstname: true, lastname: true }
|
||
});
|
||
|
||
usersWithRole.forEach(user => {
|
||
if (user.email && isValidEmail(user.email)) {
|
||
const displayName = user.name || `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.email;
|
||
recipients.push({
|
||
email: user.email,
|
||
name: displayName
|
||
});
|
||
}
|
||
});
|
||
}
|
||
break;
|
||
|
||
case 'variable':
|
||
// Get recipient from process variable
|
||
if (recipientData.recipientVariable && processVariables[recipientData.recipientVariable]) {
|
||
const variableValue = processVariables[recipientData.recipientVariable];
|
||
|
||
if (typeof variableValue === 'string' && isValidEmail(variableValue)) {
|
||
recipients.push({
|
||
email: variableValue,
|
||
name: variableValue
|
||
});
|
||
} else if (typeof variableValue === 'object' && variableValue.email && isValidEmail(variableValue.email)) {
|
||
recipients.push({
|
||
email: variableValue.email,
|
||
name: variableValue.name || variableValue.email
|
||
});
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'group':
|
||
// For future implementation - group-based recipients
|
||
console.warn('Group-based recipients not yet implemented');
|
||
break;
|
||
|
||
default:
|
||
console.warn(`Unknown recipient type: ${recipientType}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error resolving recipients:', error);
|
||
}
|
||
|
||
return recipients;
|
||
}
|
||
|
||
// Validate email address
|
||
function isValidEmail(email) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(email);
|
||
}
|
||
|
||
// Substitute variables in text
|
||
function substituteVariables(text, variables) {
|
||
if (typeof text !== 'string') return text;
|
||
|
||
// Replace {{variable}} format
|
||
text = text.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
|
||
const value = variables[varName];
|
||
if (value === undefined || value === null) return '';
|
||
if (typeof value === 'object') return JSON.stringify(value);
|
||
return String(value);
|
||
});
|
||
|
||
// Replace {variable} format
|
||
text = text.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
|
||
const value = variables[varName];
|
||
if (value === undefined || value === null) return '';
|
||
if (typeof value === 'object') return JSON.stringify(value);
|
||
return String(value);
|
||
});
|
||
|
||
return text;
|
||
}
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
try {
|
||
// Only allow POST requests
|
||
if (event.node.req.method !== 'POST') {
|
||
throw createError({
|
||
statusCode: 405,
|
||
statusMessage: 'Method Not Allowed'
|
||
});
|
||
}
|
||
|
||
// Parse request body
|
||
const body = await readBody(event);
|
||
const {
|
||
notificationType = 'info',
|
||
recipientType = 'email',
|
||
recipientData = {},
|
||
subject = '',
|
||
message = '',
|
||
processVariables = {},
|
||
processName = '',
|
||
deliveryOptions = { email: true },
|
||
priority = 'medium',
|
||
htmlMessage = '',
|
||
messageFormat = 'text'
|
||
} = body;
|
||
|
||
// Validate required fields
|
||
if (!subject && !message) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: 'Subject or message is required'
|
||
});
|
||
}
|
||
|
||
// Check if email delivery is enabled
|
||
if (!deliveryOptions.email) {
|
||
return {
|
||
success: true,
|
||
message: 'Email delivery disabled for this notification',
|
||
sent: 0
|
||
};
|
||
}
|
||
|
||
// Check environment variables
|
||
if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) {
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: 'Email configuration missing. Please set EMAIL_USER and EMAIL_PASSWORD environment variables.'
|
||
});
|
||
}
|
||
|
||
// For custom SMTP, also check required SMTP settings
|
||
const provider = process.env.EMAIL_PROVIDER?.toLowerCase() || 'custom';
|
||
if (provider === 'custom' && !process.env.SMTP_HOST) {
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: 'SMTP configuration missing. Please set SMTP_HOST for custom email provider.'
|
||
});
|
||
}
|
||
|
||
// Substitute variables in content
|
||
const processedSubject = substituteVariables(subject, processVariables);
|
||
const processedMessage = substituteVariables(message, processVariables);
|
||
const processedHtmlMessage = htmlMessage ? substituteVariables(htmlMessage, processVariables) : '';
|
||
|
||
// Resolve recipients
|
||
const recipients = await resolveRecipients(recipientType, recipientData, processVariables);
|
||
|
||
if (recipients.length === 0) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: 'No valid recipients found'
|
||
});
|
||
}
|
||
|
||
// Create transporter
|
||
const transporter = createTransporter();
|
||
|
||
// Verify connection
|
||
try {
|
||
await transporter.verify();
|
||
} catch (error) {
|
||
console.error('SMTP connection failed:', error);
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: 'Failed to connect to email server'
|
||
});
|
||
}
|
||
|
||
// Prepare email content
|
||
let emailHtml;
|
||
if (messageFormat === 'html' && processedHtmlMessage) {
|
||
emailHtml = processedHtmlMessage;
|
||
} else {
|
||
emailHtml = generateEmailTemplate(notificationType, processedSubject, processedMessage, processName);
|
||
}
|
||
|
||
// Send emails
|
||
const results = [];
|
||
let successCount = 0;
|
||
let failureCount = 0;
|
||
|
||
for (const recipient of recipients) {
|
||
try {
|
||
const mailOptions = {
|
||
from: {
|
||
name: process.env.EMAIL_FROM_NAME || 'Corrad Process System',
|
||
address: process.env.EMAIL_USER
|
||
},
|
||
to: {
|
||
name: recipient.name,
|
||
address: recipient.email
|
||
},
|
||
subject: processedSubject,
|
||
text: processedMessage,
|
||
html: emailHtml,
|
||
priority: priority === 'high' ? 'high' : priority === 'low' ? 'low' : 'normal'
|
||
};
|
||
|
||
const info = await transporter.sendMail(mailOptions);
|
||
|
||
results.push({
|
||
recipient: recipient.email,
|
||
success: true,
|
||
messageId: info.messageId
|
||
});
|
||
|
||
successCount++;
|
||
|
||
console.log(`Email sent successfully to ${recipient.email}:`, info.messageId);
|
||
} catch (error) {
|
||
results.push({
|
||
recipient: recipient.email,
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
|
||
failureCount++;
|
||
|
||
console.error(`Failed to send email to ${recipient.email}:`, error.message);
|
||
}
|
||
}
|
||
|
||
// Close transporter
|
||
transporter.close();
|
||
|
||
// Return results
|
||
return {
|
||
success: successCount > 0,
|
||
message: `Email notification processed. Sent: ${successCount}, Failed: ${failureCount}`,
|
||
sent: successCount,
|
||
failed: failureCount,
|
||
recipients: recipients.length,
|
||
details: results
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('Notification API Error:', error);
|
||
|
||
// Handle createError objects
|
||
if (error.statusCode) {
|
||
throw error;
|
||
}
|
||
|
||
// Handle other errors
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: error.message || 'Failed to send notification'
|
||
});
|
||
}
|
||
}); |