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