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"); // Initialize database and load firm data let uniqueFirms = []; let currentCampaignId = null; // Generate test recipients automatically based on test limit function generateTestRecipients(testLimit) { const baseEmail = config.email.user; // Use your own email as base const [localPart, domain] = baseEmail.split("@"); const recipients = []; for (let i = 1; i <= testLimit; i++) { // Create test recipients like: yourname+test1@gmail.com, yourname+test2@gmail.com recipients.push(`${localPart}+test${i}@${domain}`); } if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Generated ${testLimit} test recipients:`); recipients.forEach((email, index) => { console.log(`๐Ÿ› DEBUG: Recipient ${index + 1}: ${email}`); }); } return recipients; } // Extract email sending logic for reuse in retries async function sendSingleEmail(mailOptions, recipient, transporter) { if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Sending email to ${recipient}`); console.log(`๐Ÿ› DEBUG: Subject: ${mailOptions.subject}`); console.log(`๐Ÿ› DEBUG: Firm: ${mailOptions.firmName}`); console.log(`๐Ÿ› DEBUG: Tracking ID: ${mailOptions.trackingId}`); console.log( `๐Ÿ› DEBUG: HTML length: ${ mailOptions.html ? mailOptions.html.length : 0 } chars` ); console.log( `๐Ÿ› DEBUG: Text length: ${ mailOptions.text ? mailOptions.text.length : 0 } chars` ); console.log( `๐Ÿ› DEBUG: Attachments: ${ mailOptions.attachments ? mailOptions.attachments.length : 0 }` ); } await transporter.sendMail(mailOptions); // Log successful email to database if (currentCampaignId && mailOptions.firmId) { try { await database.logEmailSend({ campaignId: currentCampaignId, firmId: mailOptions.firmId, recipientEmail: recipient, subject: mailOptions.subject, status: "sent", trackingId: mailOptions.trackingId, }); } catch (dbError) { logger.error("Failed to log email send to database", { recipient, error: dbError.message, }); } } // 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: Email successfully delivered to ${recipient}`); } } async function initializeData() { try { // Initialize database await database.init(); // If in test mode with limit, we can skip migration and just create test data if (config.email.testMode && config.email.testLimit) { const testRecipients = generateTestRecipients(config.email.testLimit); if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Test limit: ${config.email.testLimit}`); } console.log( `๐Ÿงช TEST MODE: ${config.email.testLimit} email${ config.email.testLimit === 1 ? "" : "s" } to auto-generated recipients` ); // Create test firms - one for each recipient const baseTestFirms = [ { id: 1, firm_name: "Test Law Firm Alpha", location: "New York, NY", website: "https://testfirm-alpha.com", contact_email: testRecipients[0] || "test@testfirm-alpha.com", state: "New York", }, { id: 2, firm_name: "Test Law Firm Beta", location: "Los Angeles, CA", website: "https://testfirm-beta.com", contact_email: testRecipients[1] || "test@testfirm-beta.com", state: "California", }, { id: 3, firm_name: "Test Law Firm Gamma", location: "Chicago, IL", website: "https://testfirm-gamma.com", contact_email: testRecipients[2] || "test@testfirm-gamma.com", state: "Illinois", }, { id: 4, firm_name: "Test Law Firm Delta", location: "Houston, TX", website: "https://testfirm-delta.com", contact_email: testRecipients[3] || "test@testfirm-delta.com", state: "Texas", }, { id: 5, firm_name: "Test Law Firm Epsilon", location: "Miami, FL", website: "https://testfirm-epsilon.com", contact_email: testRecipients[4] || "test@testfirm-epsilon.com", state: "Florida", }, ]; uniqueFirms = baseTestFirms.slice(0, config.email.testLimit); if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Created ${uniqueFirms.length} test firms:`); uniqueFirms.forEach((firm, index) => { console.log( `๐Ÿ› DEBUG: Firm ${index + 1}: ${firm.firm_name} โ†’ ${ firm.contact_email }` ); }); } console.log( `๐Ÿ“ง Ready to send test emails using ${uniqueFirms.length} firms` ); } else { // Full initialization for production or full test runs if (fs.existsSync("firm.json")) { logger.info("Found firm.json, checking if migration is needed"); const counts = await database.getTableCounts(); if (counts.firms === 0) { logger.info("Database is empty, running migration from JSON"); // Run migration with its own database connection const { migrateJsonToDatabase, } = require("./scripts/migrate-to-database"); await migrateJsonToDatabase(); uniqueFirms = await database.getFirms(); logger.info(`Loaded ${uniqueFirms.length} firms after migration`); } else { uniqueFirms = await database.getFirms(); logger.info(`Loaded ${uniqueFirms.length} firms from database`); } } else { uniqueFirms = await database.getFirms(); logger.info(`Loaded ${uniqueFirms.length} firms from database`); } // Apply test limit if needed if ( config.email.testMode && config.email.testLimit && config.email.testLimit < uniqueFirms.length ) { uniqueFirms = uniqueFirms.slice(0, config.email.testLimit); console.log( `๐Ÿงช TEST MODE: ${config.email.testLimit} email${ config.email.testLimit === 1 ? "" : "s" } to ${config.email.testRecipient}` ); } } // Validation for test mode if (config.email.testMode && !config.email.testLimit) { throw new Error( "โŒ TEST_MODE enabled but EMAIL_TEST_LIMIT not set! Set a test limit to generate recipients automatically." ); } if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Firms to process: ${uniqueFirms.length}`); } // Create campaign currentCampaignId = await database.createCampaign({ name: `Campaign ${new Date().toISOString().split("T")[0]}`, subject: config.email.testMode ? "[TEST] Legal Partnership Opportunity" : "Legal Partnership Opportunity", templateName: "outreach", testMode: config.email.testMode, }); await database.startCampaign(currentCampaignId, uniqueFirms.length); } catch (error) { logger.error("Failed to initialize data", { error: error.message }); throw error; } } async function sendEmails() { // Initialize data first await initializeData(); // 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"); } 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`); if (config.logging.debug) { console.log( `๐Ÿ› DEBUG: Attachment files: ${attachments .map((a) => a.filename) .join(", ")}` ); } } if (config.logging.debug) { console.log( `๐Ÿ› DEBUG: SMTP Config: ${JSON.stringify( { host: config.smtp.host || "gmail", port: config.smtp.port || 587, secure: config.smtp.secure, user: config.smtp.user || config.email.user, }, null, 2 )}` ); } logger.campaignStart(uniqueFirms.length, config.email.testMode); console.log(`๐Ÿš€ Starting email campaign for ${uniqueFirms.length} firms`); for (const firm of uniqueFirms) { // Process any pending retries first await errorHandler.processRetries( transporter, async (email, recipient, trans) => { await sendSingleEmail(email, recipient, trans); } ); // Format firm data for template (database has different field names) const templateData = templateEngine.formatFirmData({ firmName: firm.firm_name, location: firm.location, website: firm.website, contactEmail: firm.contact_email, email: firm.contact_email, }); // In test mode, firm.contact_email already contains the generated test recipient // CRITICAL: In test mode, NEVER send to actual firm email const recipient = firm.contact_email; // Log what we're doing for transparency if (config.email.testMode) { const firmIndex = uniqueFirms.indexOf(firm); if (config.logging.debug) { console.log( `๐Ÿ› DEBUG: Firm ${firmIndex + 1}/${uniqueFirms.length} โ†’ ${recipient}` ); } console.log(`๐Ÿงช TEST MODE: Email for ${firm.firm_name} โ†’ ${recipient}`); } const subject = config.email.testMode ? "[TEST] Legal Partnership Opportunity" : "Legal Partnership Opportunity"; // Generate unique tracking ID for this email const trackingId = `${currentCampaignId}_${ firm.contact_email }_${Date.now()}`; // Render email using template const emailContent = await templateEngine.render("outreach", { ...templateData, subject: 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: subject, text: emailContent.text, html: trackedHtmlContent, attachments: attachments, // Store firm data for error handling and tracking firmName: firm.firm_name, firmId: firm.id, trackingId: trackingId, }; try { await sendSingleEmail(mailOptions, recipient, transporter); // Increment sent count immediately after success rateLimiter.recordSuccess(); // Show rate limiting stats const stats = rateLimiter.getStats(); console.log( `๐Ÿ“Š Progress: ${stats.sentCount} sent, ${stats.averageRate} emails/hour` ); if (config.logging.debug) { console.log( `๐Ÿ› DEBUG: Rate limiter stats: ${JSON.stringify(stats, null, 2)}` ); } } catch (error) { if (config.logging.debug) { console.log( `๐Ÿ› DEBUG: Email send failed for ${recipient}: ${error.message}` ); console.log(`๐Ÿ› DEBUG: Error code: ${error.code}`); console.log(`๐Ÿ› DEBUG: Error command: ${error.command}`); } // Use error handler for intelligent retry logic const willRetry = await errorHandler.handleError( mailOptions, recipient, error, transporter ); if (!willRetry) { console.error(`๐Ÿ’€ Skipping ${recipient} due to permanent failure`); } else if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Email queued for retry: ${recipient}`); } } // Show retry queue status const retryStats = errorHandler.getRetryStats(); if (retryStats.totalFailed > 0) { console.log( `๐Ÿ”„ Retry queue: ${retryStats.pendingRetries} pending, ${retryStats.readyToRetry} ready` ); } // Use rate limiter for intelligent delays (only if there are more emails) if (uniqueFirms.indexOf(firm) < uniqueFirms.length - 1) { const delayMs = await rateLimiter.getNextSendDelay(); console.log(`โฑ๏ธ Next email in: ${rateLimiter.formatDelay(delayMs)}`); if (config.logging.debug) { console.log(`๐Ÿ› DEBUG: Delay calculated: ${delayMs}ms`); console.log( `๐Ÿ› DEBUG: Emails remaining: ${ uniqueFirms.length - uniqueFirms.indexOf(firm) - 1 }` ); } await delay(delayMs); } } // Process any remaining retries console.log(`๐Ÿ”„ Processing final retries...`); await errorHandler.processRetries( transporter, async (email, recipient, trans) => { await sendSingleEmail(email, recipient, trans); } ); // Final stats const finalRetryStats = errorHandler.getRetryStats(); const finalRateStats = rateLimiter.getStats(); const campaignStats = { totalSent: finalRateStats.sentCount, runtime: finalRateStats.runtime, averageRate: finalRateStats.averageRate, retryStats: finalRetryStats, }; logger.campaignComplete(campaignStats); console.log(`๐Ÿ“ˆ Campaign complete. Final stats:`, campaignStats); // Complete campaign in database if (currentCampaignId) { await database.completeCampaign(currentCampaignId, { sentEmails: finalRateStats.sentCount, failedEmails: finalRetryStats.totalFailed, }); } // 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 }); } } } // Handle graceful shutdown process.on("SIGINT", async () => { console.log("\n๐Ÿ›‘ Received SIGINT. Gracefully shutting down..."); // Stop tracking server 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); }); sendEmails().catch(async (error) => { console.error("Campaign 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); });