339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
const config = require("../config");
|
|
const nodemailer = require("nodemailer");
|
|
const delay = require("delay");
|
|
const fs = require("fs");
|
|
const templateEngine = require("../lib/templateEngine");
|
|
const attachmentHandler = require("../lib/attachmentHandler");
|
|
const rateLimiter = require("../lib/rateLimiter");
|
|
const errorHandler = require("../lib/errorHandler");
|
|
const logger = require("../lib/logger");
|
|
const database = require("../lib/database");
|
|
const trackingServer = require("../lib/trackingServer");
|
|
|
|
// Extract email sending logic for reuse
|
|
async function sendSingleEmail(mailOptions, recipient, transporter) {
|
|
if (config.logging.debug) {
|
|
console.log(`🐛 DEBUG: [Campaign] Sending email to ${recipient}`);
|
|
console.log(`🐛 DEBUG: [Campaign] Subject: ${mailOptions.subject}`);
|
|
console.log(`🐛 DEBUG: [Campaign] Firm: ${mailOptions.firmName}`);
|
|
console.log(`🐛 DEBUG: [Campaign] Template: ${mailOptions.templateName}`);
|
|
console.log(`🐛 DEBUG: [Campaign] Tracking ID: ${mailOptions.trackingId}`);
|
|
}
|
|
|
|
await transporter.sendMail(mailOptions);
|
|
|
|
// Log successful email
|
|
logger.emailSent(
|
|
recipient,
|
|
mailOptions.subject,
|
|
mailOptions.firmName,
|
|
config.email.testMode
|
|
);
|
|
|
|
console.log(
|
|
`✅ Email sent to ${recipient}${
|
|
config.email.testMode ? " (TEST MODE)" : ""
|
|
}`
|
|
);
|
|
|
|
if (config.logging.debug) {
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign] Email successfully delivered to ${recipient}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Generate test recipients automatically based on campaign count
|
|
function generateCampaignTestRecipients(campaignCount) {
|
|
const baseEmail = config.email.user; // Use your own email as base
|
|
const [localPart, domain] = baseEmail.split("@");
|
|
|
|
const recipients = [];
|
|
for (let i = 1; i <= campaignCount; i++) {
|
|
// Create test recipients like: yourname+campaign1@gmail.com, yourname+campaign2@gmail.com
|
|
recipients.push(`${localPart}+campaign${i}@${domain}`);
|
|
}
|
|
|
|
if (config.logging.debug) {
|
|
console.log(
|
|
`🐛 DEBUG: Generated ${campaignCount} campaign test recipients:`
|
|
);
|
|
recipients.forEach((email, index) => {
|
|
console.log(`🐛 DEBUG: Campaign ${index + 1}: ${email}`);
|
|
});
|
|
}
|
|
|
|
return recipients;
|
|
}
|
|
|
|
async function runCampaigns() {
|
|
try {
|
|
// Initialize database
|
|
await database.init();
|
|
|
|
// Load campaign test data
|
|
const testDataFile = config.campaigns.testDataFile;
|
|
if (!fs.existsSync(testDataFile)) {
|
|
throw new Error(`Campaign test data file not found: ${testDataFile}`);
|
|
}
|
|
|
|
const testData = JSON.parse(fs.readFileSync(testDataFile, "utf8"));
|
|
let campaigns = testData.TestCampaigns || [];
|
|
|
|
if (campaigns.length === 0) {
|
|
throw new Error("No campaigns found in test data file");
|
|
}
|
|
|
|
// Generate test recipients if in test mode
|
|
if (config.email.testMode) {
|
|
const testRecipients = generateCampaignTestRecipients(campaigns.length);
|
|
|
|
// Update campaigns with generated test recipients
|
|
campaigns = campaigns.map((campaign, index) => ({
|
|
...campaign,
|
|
contactEmail: testRecipients[index] || campaign.contactEmail,
|
|
}));
|
|
|
|
console.log(
|
|
`🧪 TEST MODE: Updated ${campaigns.length} campaigns with auto-generated recipients`
|
|
);
|
|
}
|
|
|
|
console.log(`🚀 Running ${campaigns.length} different campaigns`);
|
|
|
|
if (config.logging.debug) {
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign] Loaded campaigns from: ${testDataFile}`
|
|
);
|
|
campaigns.forEach((campaign, index) => {
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign ${index + 1}] ${campaign.campaign} → ${
|
|
campaign.firmName
|
|
} → ${campaign.contactEmail}`
|
|
);
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign ${index + 1}] Subject: ${campaign.subject}`
|
|
);
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign ${index + 1}] Template: ${campaign.campaign}`
|
|
);
|
|
});
|
|
}
|
|
|
|
// Start tracking server if enabled and not skipping tracking
|
|
if (config.tracking.enabled && !config.tracking.skipTracking) {
|
|
try {
|
|
await trackingServer.start();
|
|
} catch (error) {
|
|
logger.error("Failed to start tracking server", {
|
|
error: error.message,
|
|
});
|
|
console.warn(
|
|
"⚠️ Tracking server failed to start. Continuing without tracking."
|
|
);
|
|
}
|
|
} else if (config.tracking.skipTracking) {
|
|
console.log("📊 Tracking disabled - SKIP_TRACKING enabled");
|
|
}
|
|
|
|
// Setup email transporter
|
|
const transportConfig = {
|
|
auth: {
|
|
user: config.smtp.user || config.email.user,
|
|
pass: config.smtp.pass || config.email.pass,
|
|
},
|
|
};
|
|
|
|
if (config.smtp.host) {
|
|
transportConfig.host = config.smtp.host;
|
|
transportConfig.port = config.smtp.port;
|
|
transportConfig.secure = config.smtp.secure;
|
|
} else {
|
|
transportConfig.service = "gmail";
|
|
}
|
|
|
|
const transporter = nodemailer.createTransport(transportConfig);
|
|
|
|
// Get attachments once at start
|
|
const attachments = await attachmentHandler.getAttachments();
|
|
if (attachments.length > 0) {
|
|
console.log(`📎 Attaching ${attachments.length} file(s) to emails`);
|
|
}
|
|
|
|
// Process each campaign
|
|
for (let i = 0; i < campaigns.length; i++) {
|
|
const campaign = campaigns[i];
|
|
console.log(
|
|
`\n📧 Campaign ${i + 1}/${campaigns.length}: ${campaign.campaign}`
|
|
);
|
|
console.log(` Firm: ${campaign.firmName}`);
|
|
console.log(` Template: ${campaign.campaign}`);
|
|
console.log(` Subject: ${campaign.subject}`);
|
|
|
|
// Create campaign in database
|
|
const campaignId = await database.createCampaign({
|
|
name: `${campaign.campaign} - ${
|
|
new Date().toISOString().split("T")[0]
|
|
}`,
|
|
subject: campaign.subject,
|
|
templateName: campaign.campaign,
|
|
testMode: config.email.testMode,
|
|
});
|
|
|
|
await database.startCampaign(campaignId, 1);
|
|
|
|
// Format firm data for template
|
|
const templateData = templateEngine.formatFirmData({
|
|
firmName: campaign.firmName,
|
|
location: campaign.location,
|
|
website: campaign.website,
|
|
contactEmail: campaign.contactEmail,
|
|
email: campaign.contactEmail,
|
|
state: campaign.state,
|
|
});
|
|
|
|
// Use campaign contact email directly (contains generated test recipient in test mode)
|
|
const recipient = campaign.contactEmail;
|
|
|
|
// Double check: Ensure we have a valid recipient (auto-generated in test mode)
|
|
if (config.email.testMode && !recipient) {
|
|
throw new Error("Test mode error: No recipient generated for campaign");
|
|
}
|
|
|
|
// Log what we're doing
|
|
if (config.email.testMode) {
|
|
if (config.logging.debug) {
|
|
console.log(`🐛 DEBUG: [Campaign] Campaign ${i + 1} → ${recipient}`);
|
|
}
|
|
|
|
console.log(
|
|
`🧪 TEST MODE: Email for ${campaign.firmName} → ${recipient}`
|
|
);
|
|
}
|
|
|
|
// Generate unique tracking ID
|
|
const trackingId = `${campaignId}_${campaign.contactEmail}_${Date.now()}`;
|
|
|
|
// Render email using template
|
|
const emailContent = await templateEngine.render(campaign.campaign, {
|
|
...templateData,
|
|
subject: campaign.subject,
|
|
});
|
|
|
|
// Add tracking to HTML content (unless skip tracking is enabled)
|
|
const trackedHtmlContent = trackingServer.addEmailTracking(
|
|
emailContent.html,
|
|
trackingId,
|
|
config.tracking.skipTracking
|
|
);
|
|
|
|
const mailOptions = {
|
|
from: config.email.user,
|
|
to: recipient,
|
|
subject: campaign.subject,
|
|
text: emailContent.text,
|
|
html: trackedHtmlContent,
|
|
attachments: attachments,
|
|
firmName: campaign.firmName,
|
|
trackingId: trackingId,
|
|
};
|
|
|
|
try {
|
|
await sendSingleEmail(mailOptions, recipient, transporter);
|
|
|
|
// Record success
|
|
rateLimiter.recordSuccess();
|
|
|
|
// Show progress
|
|
const stats = rateLimiter.getStats();
|
|
console.log(
|
|
`📊 Progress: ${stats.sentCount} sent, ${stats.averageRate} emails/hour`
|
|
);
|
|
|
|
// Complete campaign
|
|
await database.completeCampaign(campaignId, {
|
|
sentEmails: 1,
|
|
failedEmails: 0,
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
`❌ Failed to send campaign ${campaign.campaign}:`,
|
|
error.message
|
|
);
|
|
|
|
// Complete campaign with failure
|
|
await database.completeCampaign(campaignId, {
|
|
sentEmails: 0,
|
|
failedEmails: 1,
|
|
});
|
|
}
|
|
|
|
// Add delay between campaigns (except for the last one)
|
|
if (i < campaigns.length - 1) {
|
|
const delayMs = rateLimiter.getRandomDelay();
|
|
console.log(
|
|
`⏱️ Next campaign in: ${rateLimiter.formatDelay(delayMs)}`
|
|
);
|
|
|
|
if (config.logging.debug) {
|
|
console.log(
|
|
`🐛 DEBUG: [Campaign] Delaying ${delayMs}ms before next campaign`
|
|
);
|
|
}
|
|
|
|
// Use setTimeout wrapped in Promise instead of delay package
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
}
|
|
|
|
// Final stats
|
|
const finalStats = rateLimiter.getStats();
|
|
console.log(`\n📈 All campaigns complete! Final stats:`, finalStats);
|
|
|
|
// Stop tracking server if it was started
|
|
if (config.tracking.enabled && !config.tracking.skipTracking) {
|
|
try {
|
|
await trackingServer.stop();
|
|
} catch (error) {
|
|
logger.error("Failed to stop tracking server", {
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Campaign runner failed:", error);
|
|
|
|
// Stop tracking server on error
|
|
if (process.env.TRACKING_ENABLED === "true") {
|
|
try {
|
|
await trackingServer.stop();
|
|
} catch (stopError) {
|
|
logger.error("Failed to stop tracking server after error", {
|
|
error: stopError.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Handle graceful shutdown
|
|
process.on("SIGINT", async () => {
|
|
console.log("\n🛑 Received SIGINT. Gracefully shutting down...");
|
|
|
|
if (process.env.TRACKING_ENABLED === "true") {
|
|
try {
|
|
await trackingServer.stop();
|
|
} catch (error) {
|
|
logger.error("Failed to stop tracking server during shutdown", {
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
process.exit(0);
|
|
});
|
|
|
|
runCampaigns().catch(console.error);
|