UNPKG

@profullstack/lead-generator

Version:

A CLI tool for sending mass lead emails to prospective clients from CSV files with AI-powered personalization

234 lines (203 loc) 6.04 kB
/** * CSV Parser Module * Handles parsing and validation of lead data from CSV files */ import fs from 'fs'; import csvParser from 'csv-parser'; import { createReadStream } from 'fs'; /** * Parse CSV file and return array of lead objects * @param {string} filePath - Path to CSV file * @param {Object} options - Parsing options * @param {string} options.delimiter - CSV delimiter (default: ',') * @param {string} options.encoding - File encoding (default: 'utf8') * @returns {Promise<Array>} Array of lead objects */ export async function parseCSV(filePath, options = {}) { const { delimiter = ',', encoding = 'utf8' } = options; // Check if file exists if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } return new Promise((resolve, reject) => { const leads = []; const errors = []; createReadStream(filePath, { encoding }) .pipe(csvParser({ separator: delimiter })) .on('data', (row) => { // Clean up row data - trim whitespace and handle empty strings const cleanedRow = {}; for (const [key, value] of Object.entries(row)) { cleanedRow[key.trim()] = typeof value === 'string' ? value.trim() : value; } leads.push(cleanedRow); }) .on('end', () => { if (errors.length > 0) { reject(new Error(`CSV parsing errors: ${errors.join(', ')}`)); } else { resolve(leads); } }) .on('error', (error) => { reject(new Error(`CSV parsing failed: ${error.message}`)); }); }); } /** * Validate lead data structure and required fields * @param {Object} lead - Lead object to validate * @returns {Object} Validation result with isValid flag and errors array */ export function validateLeadData(lead) { const errors = []; const requiredFields = ['FirstName', 'Company']; // Check required fields for (const field of requiredFields) { if (!lead[field] || lead[field].trim() === '') { errors.push(`${field} is required`); } } // Check that at least one email is provided const hasWorkEmail = lead.WorkEmail && lead.WorkEmail.trim() !== ''; const hasPersonalEmail = lead.PersonalEmail && lead.PersonalEmail.trim() !== ''; if (!hasWorkEmail && !hasPersonalEmail) { errors.push('No valid email address found'); } // Validate email formats if (hasWorkEmail && !isValidEmail(lead.WorkEmail)) { errors.push('Invalid work email format'); } if (hasPersonalEmail && !isValidEmail(lead.PersonalEmail)) { errors.push('Invalid personal email format'); } return { isValid: errors.length === 0, errors }; } /** * Extract email addresses from lead data with priority * @param {Object} lead - Lead object * @returns {Object|null} Email extraction result or null if no valid emails */ export function extractEmails(lead) { const workEmail = lead.WorkEmail?.trim(); const personalEmail = lead.PersonalEmail?.trim(); // Prioritize work email over personal email if (workEmail && isValidEmail(workEmail)) { return { primary: workEmail, secondary: personalEmail && isValidEmail(personalEmail) ? personalEmail : null, type: 'work' }; } if (personalEmail && isValidEmail(personalEmail)) { return { primary: personalEmail, secondary: null, type: 'personal' }; } return null; } /** * Validate email format using regex * @param {string} email - Email address to validate * @returns {boolean} True if email format is valid */ function isValidEmail(email) { if (!email || typeof email !== 'string') { return false; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email.trim()); } /** * Process CSV file and return validated leads with email extraction * @param {string} filePath - Path to CSV file * @param {Object} options - Processing options * @returns {Promise<Object>} Processing result with valid leads, invalid leads, and stats */ export async function processLeadsFromCSV(filePath, options = {}) { try { const rawLeads = await parseCSV(filePath, options); const validLeads = []; const invalidLeads = []; const stats = { total: rawLeads.length, valid: 0, invalid: 0, workEmails: 0, personalEmails: 0 }; for (const lead of rawLeads) { const validation = validateLeadData(lead); if (validation.isValid) { const emails = extractEmails(lead); const processedLead = { ...lead, emails, validation }; validLeads.push(processedLead); stats.valid++; if (emails?.type === 'work') { stats.workEmails++; } else if (emails?.type === 'personal') { stats.personalEmails++; } } else { invalidLeads.push({ ...lead, validation }); stats.invalid++; } } return { validLeads, invalidLeads, stats }; } catch (error) { throw new Error(`Failed to process leads from CSV: ${error.message}`); } } /** * Get lead statistics from processed data * @param {Array} leads - Array of processed leads * @returns {Object} Statistics object */ export function getLeadStats(leads) { const stats = { total: leads.length, workEmails: 0, personalEmails: 0, companies: new Set(), industries: new Set() }; for (const lead of leads) { if (lead.emails?.type === 'work') { stats.workEmails++; } else if (lead.emails?.type === 'personal') { stats.personalEmails++; } if (lead.Company) { stats.companies.add(lead.Company); } if (lead.Industry) { stats.industries.add(lead.Industry); } } stats.uniqueCompanies = stats.companies.size; stats.uniqueIndustries = stats.industries.size; return stats; } export default { parseCSV, validateLeadData, extractEmails, processLeadsFromCSV, getLeadStats };