470 lines
15 KiB
JavaScript
470 lines
15 KiB
JavaScript
const nodemailer = require('nodemailer');
|
|
const emailTemplates = require('./emailTemplates');
|
|
|
|
class EmailService {
|
|
constructor() {
|
|
this.transporter = null;
|
|
this.initializeTransporter();
|
|
}
|
|
|
|
initializeTransporter() {
|
|
try {
|
|
const transporterConfig = {
|
|
host: process.env.SMTP_HOST,
|
|
port: parseInt(process.env.SMTP_PORT) || 587,
|
|
secure: process.env.SMTP_PORT === '465', // true for 465, false for other ports
|
|
tls: {
|
|
rejectUnauthorized: false
|
|
}
|
|
};
|
|
|
|
// Add auth if credentials are provided
|
|
if (process.env.SMTP_USER && process.env.SMTP_PASS) {
|
|
transporterConfig.auth = {
|
|
user: process.env.SMTP_USER,
|
|
pass: process.env.SMTP_PASS
|
|
};
|
|
}
|
|
|
|
this.transporter = nodemailer.createTransport(transporterConfig);
|
|
|
|
console.log('Email transporter initialized successfully');
|
|
} catch (error) {
|
|
console.error('Failed to initialize email transporter:', error);
|
|
}
|
|
}
|
|
|
|
async testConnection() {
|
|
try {
|
|
if (!this.transporter) {
|
|
throw new Error('Email transporter not initialized');
|
|
}
|
|
|
|
await this.transporter.verify();
|
|
return {
|
|
success: true,
|
|
message: 'SMTP connection verified successfully'
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: 'SMTP connection failed',
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
async sendEmail(emailOptions, isTest = false) {
|
|
try {
|
|
if (!this.transporter) {
|
|
throw new Error('Email transporter not initialized');
|
|
}
|
|
|
|
let to = emailOptions.to;
|
|
let subject = emailOptions.subject;
|
|
|
|
// Test mode - redirect emails and modify subject
|
|
const testMode = isTest || process.env.EMAIL_TEST_MODE === 'true';
|
|
if (testMode) {
|
|
const originalTo = to;
|
|
to = process.env.TEST_EMAIL_RECIPIENT || 'admin@example.com';
|
|
subject = `[TEST - Original: ${originalTo}] ${subject}`;
|
|
|
|
console.log(`Email redirected from ${originalTo} to ${to} (Test Mode)`);
|
|
}
|
|
|
|
const mailOptions = {
|
|
from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`,
|
|
to: to,
|
|
replyTo: emailOptions.replyTo,
|
|
subject: subject,
|
|
text: emailOptions.text,
|
|
html: emailOptions.html
|
|
};
|
|
|
|
// Log email details in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log('Email Preview:', {
|
|
to: mailOptions.to,
|
|
subject: mailOptions.subject,
|
|
preview: emailOptions.text ? emailOptions.text.substring(0, 200) + '...' : 'No text content',
|
|
testMode: testMode
|
|
});
|
|
}
|
|
|
|
console.log('DEBUG: About to send email via SMTP...');
|
|
const info = await this.transporter.sendMail(mailOptions);
|
|
|
|
console.log('DEBUG: Email sent via SMTP successfully:', info.messageId);
|
|
console.log('DEBUG: Email info response:', info.response);
|
|
|
|
// Log email to database if NocoDB service is available
|
|
console.log('DEBUG: About to log email to database...');
|
|
try {
|
|
await this.logEmailSent({
|
|
to: emailOptions.to, // Log original recipient
|
|
subject: emailOptions.subject, // Log original subject
|
|
status: 'sent',
|
|
messageId: info.messageId,
|
|
testMode: testMode,
|
|
senderName: emailOptions.from?.name || 'System',
|
|
senderEmail: emailOptions.from?.email || process.env.SMTP_FROM_EMAIL
|
|
});
|
|
console.log('DEBUG: Successfully logged email to database');
|
|
} catch (logError) {
|
|
console.error('DEBUG: Failed to log email to database:', logError);
|
|
// Continue anyway - don't let logging failure affect email success
|
|
}
|
|
|
|
const successResult = {
|
|
success: true,
|
|
messageId: info.messageId,
|
|
response: info.response,
|
|
testMode: testMode,
|
|
originalRecipient: testMode ? emailOptions.to : undefined
|
|
};
|
|
|
|
console.log('DEBUG: Returning success result:', successResult);
|
|
return successResult;
|
|
} catch (error) {
|
|
console.error('Email send error:', error);
|
|
|
|
// Log failed email attempt
|
|
await this.logEmailSent({
|
|
to: emailOptions.to,
|
|
subject: emailOptions.subject,
|
|
status: 'failed',
|
|
error: error.message,
|
|
testMode: isTest || process.env.EMAIL_TEST_MODE === 'true',
|
|
senderName: emailOptions.from?.name || 'System',
|
|
senderEmail: emailOptions.from?.email || process.env.SMTP_FROM_EMAIL
|
|
});
|
|
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
async sendBulkEmails(emails) {
|
|
const results = [];
|
|
|
|
for (const email of emails) {
|
|
try {
|
|
const result = await this.sendEmail(email);
|
|
results.push({
|
|
to: email.to,
|
|
success: result.success,
|
|
messageId: result.messageId,
|
|
error: result.error
|
|
});
|
|
|
|
// Add a small delay between emails to avoid overwhelming the SMTP server
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
} catch (error) {
|
|
results.push({
|
|
to: email.to,
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
formatEmailTemplate(template, data) {
|
|
let formattedTemplate = template;
|
|
|
|
// Replace placeholders with actual data
|
|
Object.keys(data).forEach(key => {
|
|
const placeholder = `{{${key}}}`;
|
|
formattedTemplate = formattedTemplate.replace(new RegExp(placeholder, 'g'), data[key]);
|
|
});
|
|
|
|
return formattedTemplate;
|
|
}
|
|
|
|
validateEmailAddress(email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
async logEmailSent(emailData) {
|
|
try {
|
|
// Use the existing logEmailSend method with the correct field structure
|
|
const nocodbService = require('./nocodb');
|
|
if (nocodbService && process.env.NOCODB_TABLE_EMAILS) {
|
|
await nocodbService.logEmailSend({
|
|
recipientEmail: emailData.to,
|
|
senderName: emailData.senderName || 'System',
|
|
senderEmail: emailData.senderEmail || process.env.SMTP_FROM_EMAIL,
|
|
subject: emailData.subject,
|
|
postalCode: emailData.postalCode || 'N/A',
|
|
status: emailData.status || 'sent',
|
|
timestamp: new Date().toISOString(),
|
|
senderIP: emailData.senderIP || 'localhost'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to log email:', error);
|
|
// Don't throw - logging failure shouldn't prevent email sending
|
|
}
|
|
}
|
|
|
|
async previewEmail(emailOptions) {
|
|
// Generate email preview without sending
|
|
return {
|
|
to: emailOptions.to,
|
|
subject: emailOptions.subject,
|
|
body: emailOptions.text,
|
|
html: emailOptions.html,
|
|
from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`,
|
|
replyTo: emailOptions.replyTo,
|
|
timestamp: new Date().toISOString(),
|
|
testMode: process.env.EMAIL_TEST_MODE === 'true',
|
|
redirectTo: process.env.EMAIL_TEST_MODE === 'true' ? process.env.TEST_EMAIL_RECIPIENT : null
|
|
};
|
|
}
|
|
|
|
// Template-based email methods
|
|
async sendTemplatedEmail(templateName, templateVariables, emailOptions, isTest = false) {
|
|
console.log('DEBUG: sendTemplatedEmail called with template:', templateName);
|
|
try {
|
|
// Render the template
|
|
console.log('DEBUG: About to render template...');
|
|
const { html, text } = await emailTemplates.render(templateName, templateVariables);
|
|
console.log('DEBUG: Template rendered successfully');
|
|
|
|
// Prepare email options with rendered content
|
|
const mailOptions = {
|
|
...emailOptions,
|
|
text: text,
|
|
html: html
|
|
};
|
|
|
|
// Send the email using existing sendEmail method
|
|
console.log('DEBUG: About to call sendEmail from sendTemplatedEmail...');
|
|
const result = await this.sendEmail(mailOptions, isTest);
|
|
console.log('DEBUG: sendEmail returned result:', result);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('DEBUG: Failed to send templated email:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
async sendRepresentativeEmail(recipientEmail, senderName, senderEmail, subject, message, postalCode, recipientName = null) {
|
|
// Generate dynamic subject if not provided
|
|
const finalSubject = subject || `Message from ${senderName} from ${postalCode}`;
|
|
|
|
const templateVariables = {
|
|
MESSAGE: message,
|
|
SENDER_NAME: senderName,
|
|
SENDER_EMAIL: senderEmail,
|
|
POSTAL_CODE: postalCode,
|
|
RECIPIENT_NAME: recipientName || 'Representative'
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: recipientEmail,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
replyTo: senderEmail,
|
|
subject: finalSubject
|
|
};
|
|
|
|
return await this.sendTemplatedEmail('representative-contact', templateVariables, emailOptions);
|
|
}
|
|
|
|
async sendCampaignEmail(recipientEmail, userEmail, userName, postalCode, subject, message, campaignTitle, recipientName = null, recipientLevel = null) {
|
|
console.log('DEBUG: sendCampaignEmail called for recipient:', recipientEmail);
|
|
const templateVariables = {
|
|
MESSAGE: message,
|
|
USER_NAME: userName,
|
|
USER_EMAIL: userEmail,
|
|
POSTAL_CODE: postalCode,
|
|
CAMPAIGN_TITLE: campaignTitle,
|
|
RECIPIENT_NAME: recipientName,
|
|
RECIPIENT_LEVEL: recipientLevel
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: recipientEmail,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
replyTo: userEmail,
|
|
subject: subject
|
|
};
|
|
|
|
console.log('DEBUG: About to call sendTemplatedEmail from sendCampaignEmail...');
|
|
const result = await this.sendTemplatedEmail('campaign-email', templateVariables, emailOptions);
|
|
console.log('DEBUG: sendCampaignEmail received result:', result);
|
|
return result;
|
|
}
|
|
|
|
async sendTestEmail(subject, message, testRecipient = null) {
|
|
const recipient = testRecipient || process.env.TEST_EMAIL_RECIPIENT || 'admin@example.com';
|
|
|
|
const templateVariables = {
|
|
MESSAGE: message
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: recipient,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
replyTo: process.env.SMTP_FROM_EMAIL,
|
|
subject: `[TEST EMAIL] ${subject}`
|
|
};
|
|
|
|
return await this.sendTemplatedEmail('test-email', templateVariables, emailOptions, true);
|
|
}
|
|
|
|
async previewTemplatedEmail(templateName, templateVariables, emailOptions) {
|
|
try {
|
|
const { html, text } = await emailTemplates.render(templateName, templateVariables);
|
|
|
|
return {
|
|
to: emailOptions.to,
|
|
subject: emailOptions.subject,
|
|
body: text,
|
|
html: html,
|
|
from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`,
|
|
replyTo: emailOptions.replyTo,
|
|
timestamp: new Date().toISOString(),
|
|
testMode: process.env.EMAIL_TEST_MODE === 'true',
|
|
redirectTo: process.env.EMAIL_TEST_MODE === 'true' ? process.env.TEST_EMAIL_RECIPIENT : null,
|
|
templateName: templateName,
|
|
templateVariables: templateVariables
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to preview templated email:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// User management email methods
|
|
async sendLoginDetails(user) {
|
|
try {
|
|
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3333}`;
|
|
|
|
const isAdmin = user.admin || user.Admin || false;
|
|
|
|
const templateVariables = {
|
|
APP_NAME: 'BNKops Influence',
|
|
USER_NAME: user.Name || user.name || user.Email || user.email,
|
|
USER_EMAIL: user.Email || user.email,
|
|
PASSWORD: user.Password || user.password,
|
|
USER_ROLE: isAdmin ? 'Administrator' : 'User',
|
|
LOGIN_URL: `${baseUrl}/login.html`,
|
|
TIMESTAMP: new Date().toLocaleString()
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: user.Email || user.email,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
subject: `Your Login Details - ${templateVariables.APP_NAME}`
|
|
};
|
|
|
|
return await this.sendTemplatedEmail('login-details', templateVariables, emailOptions);
|
|
} catch (error) {
|
|
console.error('Failed to send login details email:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async sendEmailVerification(recipientEmail, verificationUrl, userName) {
|
|
try {
|
|
const templateVariables = {
|
|
USER_NAME: userName || 'there',
|
|
VERIFICATION_URL: verificationUrl
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: recipientEmail,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
subject: 'Verify Your Email to Create Your Campaign'
|
|
};
|
|
|
|
return await this.sendTemplatedEmail('email-verification', templateVariables, emailOptions);
|
|
} catch (error) {
|
|
console.error('Failed to send email verification:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send response verification email to representative
|
|
* @param {Object} options - Email options
|
|
* @param {string} options.representativeEmail - Representative's email address
|
|
* @param {string} options.representativeName - Representative's name
|
|
* @param {string} options.campaignTitle - Campaign title
|
|
* @param {string} options.responseType - Type of response (Email, Letter, etc.)
|
|
* @param {string} options.responseText - The actual response text
|
|
* @param {string} options.submittedDate - Date the response was submitted
|
|
* @param {string} options.submitterName - Name of person who submitted
|
|
* @param {string} options.verificationUrl - URL to verify the response
|
|
* @param {string} options.reportUrl - URL to report as invalid
|
|
*/
|
|
async sendResponseVerification(options) {
|
|
try {
|
|
const {
|
|
representativeEmail,
|
|
representativeName,
|
|
campaignTitle,
|
|
responseType,
|
|
responseText,
|
|
submittedDate,
|
|
submitterName,
|
|
verificationUrl,
|
|
reportUrl
|
|
} = options;
|
|
|
|
const templateVariables = {
|
|
REPRESENTATIVE_NAME: representativeName,
|
|
CAMPAIGN_TITLE: campaignTitle,
|
|
RESPONSE_TYPE: responseType,
|
|
RESPONSE_TEXT: responseText,
|
|
SUBMITTED_DATE: submittedDate,
|
|
SUBMITTER_NAME: submitterName || 'Anonymous',
|
|
VERIFICATION_URL: verificationUrl,
|
|
REPORT_URL: reportUrl,
|
|
APP_NAME: process.env.APP_NAME || 'BNKops Influence',
|
|
TIMESTAMP: new Date().toLocaleString()
|
|
};
|
|
|
|
const emailOptions = {
|
|
to: representativeEmail,
|
|
from: {
|
|
email: process.env.SMTP_FROM_EMAIL,
|
|
name: process.env.SMTP_FROM_NAME
|
|
},
|
|
subject: `Response Verification Request - ${campaignTitle}`
|
|
};
|
|
|
|
return await this.sendTemplatedEmail('response-verification', templateVariables, emailOptions);
|
|
} catch (error) {
|
|
console.error('Failed to send response verification email:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new EmailService(); |