outreach/scripts/run-campaigns.js
2025-08-15 01:03:38 -08:00

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);