Complete Guide to Personalized Email Campaigns with Node.js and TypeScript

Complete Guide to Personalized Email Campaigns with Node.js and TypeScript

Build a Production-Ready Email Campaign System with Modern JavaScript

While Python excels at scripting, Node.js offers superior performance for I/O-heavy operations like email sending, especially when handling thousands of recipients. This guide provides a complete, production-ready email campaign system using TypeScript, Node.js, and modern best practices.

Why Node.js for Email Campaigns?

  1. Non-blocking I/O: Send multiple emails concurrently without waiting for each to complete
  2. Real-time Processing: Better for handling large batches and streaming data
  3. Type Safety: TypeScript catches errors at compile time, not runtime
  4. Modern Ecosystem: Access to thousands of specialized npm packages
  5. Better Error Handling: Async/await makes complex workflows more manageable

Project Structure

email-campaign-system/
├── src/
│   ├── config/
│   │   └── index.ts
│   ├── core/
│   │   ├── CampaignEngine.ts
│   │   ├── EmailComposer.ts
│   │   ├── TemplateEngine.ts
│   │   └── types/
│   │       └── index.ts
│   ├── services/
│   │   ├── EmailService.ts
│   │   ├── LoggerService.ts
│   │   └── StorageService.ts
│   ├── templates/
│   │   └── newsletter.html
│   ├── data/
│   │   └── contacts.csv
│   └── index.ts
├── logs/
├── dist/
├── package.json
├── tsconfig.json
├── .env.example
└── README.md

Installation & Setup

# Initialize project
mkdir email-campaign-system
cd email-campaign-system
npm init -y

# Install dependencies
npm install typescript ts-node nodemailer csv-parser dotenv winston
npm install -D @types/node @types/nodemailer @types/csv-parser concurrently

# Initialize TypeScript
npx tsc --init

Configuration

.env file

# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false  # true for 465, false for other ports
[email protected]
SMTP_PASSWORD=your_app_password

# Sender Information
SENDER_NAME=Your Company
[email protected]
[email protected]

# Campaign Settings
CAMPAIGN_NAME=Monthly_Newsletter
BATCH_SIZE=50
DELAY_MS=2000
MAX_RETRIES=3

# Paths
CONTACTS_CSV=./data/contacts.csv
TEMPLATE_DIR=./src/templates
LOG_DIR=./logs

src/config/index.ts

import dotenv from 'dotenv';
import path from 'path';

dotenv.config();

export interface EmailConfig {
  smtp: {
    host: string;
    port: number;
    secure: boolean;
    auth: {
      user: string;
      pass: string;
    };
  };
  sender: {
    name: string;
    email: string;
    replyTo: string;
  };
  campaign: {
    name: string;
    batchSize: number;
    delayMs: number;
    maxRetries: number;
  };
  paths: {
    contactsCsv: string;
    templateDir: string;
    logDir: string;
  };
  security: {
    unsubscribeSecret: string;
    trackingSecret: string;
  };
}

export const config: EmailConfig = {
  smtp: {
    host: process.env.SMTP_HOST || 'smtp.gmail.com',
    port: parseInt(process.env.SMTP_PORT || '587'),
    secure: process.env.SMTP_SECURE === 'true',
    auth: {
      user: process.env.SMTP_USER || '',
      pass: process.env.SMTP_PASSWORD || '',
    },
  },
  sender: {
    name: process.env.SENDER_NAME || 'Your Company',
    email: process.env.SENDER_EMAIL || '',
    replyTo: process.env.REPLY_TO || process.env.SENDER_EMAIL || '',
  },
  campaign: {
    name: process.env.CAMPAIGN_NAME || 'Default_Campaign',
    batchSize: parseInt(process.env.BATCH_SIZE || '50'),
    delayMs: parseInt(process.env.DELAY_MS || '2000'),
    maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
  },
  paths: {
    contactsCsv: process.env.CONTACTS_CSV || './data/contacts.csv',
    templateDir: process.env.TEMPLATE_DIR || './src/templates',
    logDir: process.env.LOG_DIR || './logs',
  },
  security: {
    unsubscribeSecret: process.env.UNSUBSCRIBE_SECRET || 'change-this-secret',
    trackingSecret: process.env.TRACKING_SECRET || 'change-this-tracking-secret',
  },
};

// Validate required configuration
if (!config.smtp.auth.user || !config.smtp.auth.pass) {
  throw new Error('SMTP credentials are not configured. Check your .env file.');
}

Type Definitions

src/core/types/index.ts

export interface Contact {
  email: string;
  name: string;
  [key: string]: string | number | boolean | null;
}

export interface CampaignStats {
  total: number;
  sent: number;
  failed: number;
  skipped: number;
  invalid: number;
  startTime: Date | null;
  endTime: Date | null;
  durationMs: number;
}

export interface EmailOptions {
  to: string;
  subject: string;
  html: string;
  text?: string;
  replyTo?: string;
  trackingId?: string;
  metadata?: Record<string, any>;
}

export interface SendResult {
  success: boolean;
  contact: Contact;
  messageId?: string;
  error?: string;
  retryCount: number;
  timestamp: Date;
}

export interface TemplateVariables {
  [key: string]: string | number | boolean | null;
}

export interface BatchProgress {
  batchNumber: number;
  totalBatches: number;
  processedInBatch: number;
  totalProcessed: number;
  estimatedTimeRemaining: number;
}

Logger Service

src/services/LoggerService.ts

import winston from 'winston';
import path from 'path';
import fs from 'fs';
import { config } from '../config';

// Ensure log directory exists
if (!fs.existsSync(config.paths.logDir)) {
  fs.mkdirSync(config.paths.logDir, { recursive: true });
}

const logFormat = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.errors({ stack: true }),
  winston.format.splat(),
  winston.format.json()
);

const consoleFormat = winston.format.combine(
  winston.format.colorize(),
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.printf(
    ({ timestamp, level, message, ...meta }) =>
      `${timestamp} [${level}]: ${message} ${
        Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
      }`
  )
);

export class LoggerService {
  private static instance: winston.Logger;

  static getInstance(): winston.Logger {
    if (!LoggerService.instance) {
      LoggerService.instance = winston.createLogger({
        level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
        format: logFormat,
        defaultMeta: { service: 'email-campaign' },
        transports: [
          new winston.transports.File({
            filename: path.join(config.paths.logDir, 'error.log'),
            level: 'error',
            maxsize: 5242880, // 5MB
            maxFiles: 5,
          }),
          new winston.transports.File({
            filename: path.join(config.paths.logDir, 'combined.log'),
            maxsize: 5242880, // 5MB
            maxFiles: 5,
          }),
          new winston.transports.Console({
            format: consoleFormat,
          }),
        ],
      });
    }
    return LoggerService.instance;
  }

  static info(message: string, meta?: any) {
    LoggerService.getInstance().info(message, meta);
  }

  static error(message: string, meta?: any) {
    LoggerService.getInstance().error(message, meta);
  }

  static warn(message: string, meta?: any) {
    LoggerService.getInstance().warn(message, meta);
  }

  static debug(message: string, meta?: any) {
    LoggerService.getInstance().debug(message, meta);
  }

  static campaignStarted(contactCount: number) {
    LoggerService.info('Campaign started', { contactCount });
  }

  static campaignCompleted(stats: any) {
    LoggerService.info('Campaign completed', { stats });
  }

  static emailSent(contact: any, messageId?: string) {
    LoggerService.info('Email sent successfully', { 
      email: contact.email, 
      name: contact.name,
      messageId 
    });
  }

  static emailFailed(contact: any, error: string) {
    LoggerService.error('Failed to send email', { 
      email: contact.email, 
      name: contact.name,
      error 
    });
  }
}

Template Engine

src/core/TemplateEngine.ts

import fs from 'fs';
import path from 'path';
import { LoggerService } from '../services/LoggerService';
import { config } from '../config';
import { TemplateVariables } from './types';

export class TemplateEngine {
  private templates: Map<string, string> = new Map();
  private compiledTemplates: Map<string, Function> = new Map();

  constructor(private templateDir: string = config.paths.templateDir) {
    this.loadTemplates();
  }

  private loadTemplates(): void {
    try {
      if (!fs.existsSync(this.templateDir)) {
        fs.mkdirSync(this.templateDir, { recursive: true });
        LoggerService.warn(`Template directory created: ${this.templateDir}`);
        return;
      }

      const files = fs.readdirSync(this.templateDir);
      files.forEach(file => {
        if (file.endsWith('.html')) {
          const templatePath = path.join(this.templateDir, file);
          const templateName = path.basename(file, '.html');
          const content = fs.readFileSync(templatePath, 'utf-8');
          this.templates.set(templateName, content);
          this.compileTemplate(templateName, content);
          LoggerService.debug(`Loaded template: ${templateName}`);
        }
      });

      LoggerService.info(`Loaded ${this.templates.size} templates`);
    } catch (error) {
      LoggerService.error('Failed to load templates', { error });
      throw error;
    }
  }

  private compileTemplate(name: string, content: string): void {
    try {
      // Simple template compilation - replace {variable} with dynamic values
      const compiled = (variables: TemplateVariables): string => {
        let result = content;

        // Replace all {variable} placeholders
        Object.entries(variables).forEach(([key, value]) => {
          const placeholder = new RegExp(`\\{${key}\\}`, 'g');
          result = result.replace(placeholder, String(value || ''));
        });

        // Handle conditional blocks (simple implementation)
        result = this.processConditionals(result, variables);

        // Handle loops (simple implementation)
        result = this.processLoops(result, variables);

        return result;
      };

      this.compiledTemplates.set(name, compiled);
    } catch (error) {
      LoggerService.error(`Failed to compile template: ${name}`, { error });
      throw error;
    }
  }

  private processConditionals(content: string, variables: TemplateVariables): string {
    // Simple conditional: <!-- if:variable --> content <!-- endif -->
    const conditionalRegex = /<!--\s*if:(\w+)\s*-->(.*?)<!--\s*endif\s*-->/gs;

    return content.replace(conditionalRegex, (match, variableName, conditionalContent) => {
      if (variables[variableName]) {
        return conditionalContent;
      }
      return '';
    });
  }

  private processLoops(content: string, variables: TemplateVariables): string {
    // Simple loop: <!-- loop:arrayName --> content <!-- endloop -->
    const loopRegex = /<!--\s*loop:(\w+)\s*-->(.*?)<!--\s*endloop\s*-->/gs;

    return content.replace(loopRegex, (match, arrayName, loopContent) => {
      const array = variables[arrayName];
      if (Array.isArray(array)) {
        return array.map(item => {
          let itemContent = loopContent;
          Object.entries(item).forEach(([key, value]) => {
            const placeholder = new RegExp(`\\{item\\.${key}\\}`, 'g');
            itemContent = itemContent.replace(placeholder, String(value || ''));
          });
          return itemContent;
        }).join('');
      }
      return '';
    });
  }

  render(templateName: string, variables: TemplateVariables): string {
    const compiled = this.compiledTemplates.get(templateName);
    if (!compiled) {
      throw new Error(`Template not found: ${templateName}`);
    }

    try {
      return compiled(variables);
    } catch (error) {
      LoggerService.error(`Failed to render template: ${templateName}`, { 
        error, 
        variables 
      });
      throw error;
    }
  }

  htmlToText(html: string): string {
    // Basic HTML to text conversion
    return html
      .replace(/<br\s*\/?>/gi, '\n')
      .replace(/<\/p>/gi, '\n\n')
      .replace(/<[^>]+>/g, '')
      .replace(/&nbsp;/g, ' ')
      .replace(/&amp;/g, '&')
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&quot;/g, '"')
      .replace(/'/g, "'")
      .trim();
  }

  validateTemplate(templateName: string, requiredVariables: string[]): string[] {
    const template = this.templates.get(templateName);
    if (!template) {
      throw new Error(`Template not found: ${templateName}`);
    }

    const missingVariables: string[] = [];
    requiredVariables.forEach(variable => {
      const placeholder = `{${variable}}`;
      if (!template.includes(placeholder)) {
        missingVariables.push(variable);
      }
    });

    return missingVariables;
  }

  listTemplates(): string[] {
    return Array.from(this.templates.keys());
  }

  addTemplate(name: string, content: string): void {
    this.templates.set(name, content);
    this.compileTemplate(name, content);

    // Save to file
    const filePath = path.join(this.templateDir, `${name}.html`);
    fs.writeFileSync(filePath, content, 'utf-8');

    LoggerService.info(`Template added: ${name}`);
  }
}

Email Composer

src/core/EmailComposer.ts

import { config } from '../config';
import { TemplateEngine } from './TemplateEngine';
import { Contact, EmailOptions, TemplateVariables } from './types';
import { LoggerService } from '../services/LoggerService';
import crypto from 'crypto';

export class EmailComposer {
  constructor(
    private templateEngine: TemplateEngine,
    private campaignName: string = config.campaign.name
  ) {}

  generateTrackingId(contact: Contact): string {
    const data = `${contact.email}|${this.campaignName}|${Date.now()}`;
    return crypto
      .createHash('sha256')
      .update(data)
      .digest('hex')
      .substring(0, 16);
  }

  generateUnsubscribeLink(email: string): string {
    const token = crypto
      .createHmac('sha256', config.security.unsubscribeSecret)
      .update(email)
      .digest('hex');

    return `https://yourdomain.com/unsubscribe?email=${encodeURIComponent(email)}&token=${token}`;
  }

  generateTrackingPixel(trackingId: string): string {
    const encodedId = Buffer.from(trackingId).toString('base64');
    return `https://yourdomain.com/track/open/${encodedId}.png`;
  }

  createPersonalizedVariables(contact: Contact): TemplateVariables {
    const trackingId = this.generateTrackingId(contact);
    const unsubscribeLink = this.generateUnsubscribeLink(contact.email);
    const trackingPixel = this.generateTrackingPixel(trackingId);

    return {
      ...contact,
      campaign_name: this.campaignName,
      current_year: new Date().getFullYear(),
      current_date: new Date().toLocaleDateString(),
      unsubscribe_url: unsubscribeLink,
      tracking_pixel: trackingPixel,
      tracking_id: trackingId,
      // Add computed fields
      first_name: contact.name.split(' ')[0],
      company_initial: contact.company ? (contact.company as string).charAt(0) : '',
    };
  }

  createEmailOptions(
    contact: Contact,
    templateName: string = 'newsletter'
  ): EmailOptions {
    const variables = this.createPersonalizedVariables(contact);

    // Render HTML content
    const htmlContent = this.templateEngine.render(templateName, variables);

    // Generate plain text version
    const textContent = this.templateEngine.htmlToText(htmlContent);

    // Personalize subject line
    const subject = this.personalizeSubject(contact);

    return {
      to: contact.email,
      subject,
      html: htmlContent,
      text: textContent,
      replyTo: config.sender.replyTo,
      trackingId: variables.tracking_id as string,
      metadata: {
        campaign: this.campaignName,
        contactId: contact.email,
        sentAt: new Date().toISOString(),
      },
    };
  }

  private personalizeSubject(contact: Contact): string {
    const baseSubject = `Updates for ${contact.company || 'your business'}`;

    // Add personalization tokens
    const tokens = [
      contact.name ? `Hi ${contact.name.split(' ')[0]}, ` : '',
      contact.company ? `for ${contact.company}` : '',
    ].filter(Boolean);

    return tokens.length > 0 ? `${tokens.join(' ')} - ${baseSubject}` : baseSubject;
  }

  validateContact(contact: Contact): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    // Email validation
    if (!contact.email || !this.isValidEmail(contact.email)) {
      errors.push('Invalid email address');
    }

    // Name validation
    if (!contact.name || contact.name.trim().length < 2) {
      errors.push('Name is required and must be at least 2 characters');
    }

    return {
      valid: errors.length === 0,
      errors,
    };
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  batchCreateEmailOptions(
    contacts: Contact[],
    templateName: string = 'newsletter'
  ): { valid: EmailOptions[]; invalid: Contact[] } {
    const validEmails: EmailOptions[] = [];
    const invalidContacts: Contact[] = [];

    contacts.forEach(contact => {
      const validation = this.validateContact(contact);

      if (validation.valid) {
        try {
          const emailOptions = this.createEmailOptions(contact, templateName);
          validEmails.push(emailOptions);
        } catch (error) {
          LoggerService.error('Failed to create email options', {
            contact,
            error: error instanceof Error ? error.message : String(error),
          });
          invalidContacts.push(contact);
        }
      } else {
        LoggerService.warn('Invalid contact skipped', {
          contact,
          errors: validation.errors,
        });
        invalidContacts.push(contact);
      }
    });

    return { valid: validEmails, invalid: invalidContacts };
  }
}

Email Service

src/services/EmailService.ts

import nodemailer from 'nodemailer';
import { LoggerService } from './LoggerService';
import { config } from '../config';
import { EmailOptions, SendResult } from '../core/types';

export class EmailService {
  private transporter: nodemailer.Transporter;
  private connectionTested: boolean = false;

  constructor() {
    this.transporter = nodemailer.createTransport({
      host: config.smtp.host,
      port: config.smtp.port,
      secure: config.smtp.secure,
      auth: {
        user: config.smtp.auth.user,
        pass: config.smtp.auth.pass,
      },
      // Connection pooling
      pool: true,
      maxConnections: 5,
      maxMessages: 100,
    });

    // Event handlers
    this.transporter.on('idle', () => {
      LoggerService.debug('SMTP connection is idle');
    });

    this.transporter.on('error', (error) => {
      LoggerService.error('SMTP connection error', { error });
    });
  }

  async testConnection(): Promise<boolean> {
    if (this.connectionTested) {
      return true;
    }

    try {
      LoggerService.info('Testing SMTP connection...');
      await this.transporter.verify();
      this.connectionTested = true;
      LoggerService.info('SMTP connection verified successfully');
      return true;
    } catch (error) {
      LoggerService.error('SMTP connection test failed', { error });
      return false;
    }
  }

  async sendEmail(options: EmailOptions, retryCount = 0): Promise<SendResult> {
    const startTime = Date.now();

    try {
      const mailOptions = {
        from: `${config.sender.name} <${config.sender.email}>`,
        to: options.to,
        replyTo: options.replyTo || config.sender.replyTo,
        subject: options.subject,
        html: options.html,
        text: options.text,
        // Add headers for tracking
        headers: {
          'X-Campaign-ID': config.campaign.name,
          'X-Contact-ID': options.trackingId || '',
          'X-Mailer': 'Node.js Campaign System',
          'List-Unsubscribe': `<${this.generateUnsubscribeHeader(options.to)}>`,
        },
      };

      const info = await this.transporter.sendMail(mailOptions);
      const duration = Date.now() - startTime;

      LoggerService.debug('Email sent successfully', {
        messageId: info.messageId,
        duration,
        to: options.to,
      });

      return {
        success: true,
        contact: { email: options.to, name: '' }, // Will be populated by caller
        messageId: info.messageId,
        retryCount,
        timestamp: new Date(),
      };
    } catch (error) {
      const duration = Date.now() - startTime;
      const errorMessage = error instanceof Error ? error.message : String(error);

      LoggerService.error('Failed to send email', {
        to: options.to,
        error: errorMessage,
        duration,
        retryCount,
      });

      // Retry logic
      if (retryCount < config.campaign.maxRetries) {
        const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff
        LoggerService.info(`Retrying email (attempt ${retryCount + 1}/${config.campaign.maxRetries})`, {
          to: options.to,
          delay,
        });

        await this.delay(delay);
        return this.sendEmail(options, retryCount + 1);
      }

      return {
        success: false,
        contact: { email: options.to, name: '' },
        error: errorMessage,
        retryCount,
        timestamp: new Date(),
      };
    }
  }

  async sendBatch(
    emails: EmailOptions[],
    delayMs: number = config.campaign.delayMs,
    progressCallback?: (progress: any) => void
  ): Promise<SendResult[]> {
    const results: SendResult[] = [];

    for (let i = 0; i < emails.length; i++) {
      const email = emails[i];

      try {
        const result = await this.sendEmail(email);
        results.push(result);

        // Update progress
        if (progressCallback) {
          progressCallback({
            current: i + 1,
            total: emails.length,
            percentage: Math.round(((i + 1) / emails.length) * 100),
            lastResult: result,
          });
        }

        // Add delay between emails (except the last one)
        if (i < emails.length - 1 && delayMs > 0) {
          await this.delay(delayMs);
        }
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        results.push({
          success: false,
          contact: { email: email.to, name: '' },
          error: errorMessage,
          retryCount: 0,
          timestamp: new Date(),
        });
      }
    }

    return results;
  }

  async sendBatchParallel(
    emails: EmailOptions[],
    concurrency: number = 5
  ): Promise<SendResult[]> {
    const results: SendResult[] = [];
    const queue = [...emails];

    const worker = async (): Promise<void> => {
      while (queue.length > 0) {
        const email = queue.shift();
        if (email) {
          const result = await this.sendEmail(email);
          results.push(result);
        }
      }
    };

    // Create worker pool
    const workers = Array(concurrency).fill(null).map(() => worker());
    await Promise.all(workers);

    return results;
  }

  private generateUnsubscribeHeader(email: string): string {
    // Generate unsubscribe URL
    return `https://yourdomain.com/unsubscribe?email=${encodeURIComponent(email)}`;
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async close(): Promise<void> {
    try {
      await this.transporter.close();
      LoggerService.info('SMTP transporter closed');
    } catch (error) {
      LoggerService.error('Error closing SMTP transporter', { error });
    }
  }

  getStats(): any {
    return {
      connected: this.connectionTested,
      maxConnections: (this.transporter as any).options.maxConnections,
      pool: (this.transporter as any).options.pool,
    };
  }
}

Campaign Engine

src/core/CampaignEngine.ts

import fs from 'fs';
import path from 'path';
import csvParser from 'csv-parser';
import { LoggerService } from '../services/LoggerService';
import { EmailComposer } from './EmailComposer';
import { EmailService } from '../services/EmailService';
import { TemplateEngine } from './TemplateEngine';
import { config } from '../config';
import {
  Contact,
  CampaignStats,
  SendResult,
  BatchProgress,
} from './types';

export class CampaignEngine {
  private stats: CampaignStats = {
    total: 0,
    sent: 0,
    failed: 0,
    skipped: 0,
    invalid: 0,
    startTime: null,
    endTime: null,
    durationMs: 0,
  };

  private results: SendResult[] = [];
  private invalidContacts: Contact[] = [];

  constructor(
    private emailComposer: EmailComposer,
    private emailService: EmailService,
    private templateEngine: TemplateEngine
  ) {}

  async loadContacts(csvPath: string = config.paths.contactsCsv): Promise<Contact[]> {
    return new Promise((resolve, reject) => {
      const contacts: Contact[] = [];

      if (!fs.existsSync(csvPath)) {
        reject(new Error(`Contacts file not found: ${csvPath}`));
        return;
      }

      fs.createReadStream(csvPath)
        .pipe(csvParser())
        .on('data', (row) => {
          // Clean and validate each row
          const cleanedRow: Contact = { email: '', name: '' };

          Object.entries(row).forEach(([key, value]) => {
            cleanedRow[key.trim().toLowerCase()] = String(value).trim();
          });

          // Ensure email and name exist
          if (cleanedRow.email && cleanedRow.name) {
            contacts.push(cleanedRow);
          }
        })
        .on('end', () => {
          LoggerService.info(`Loaded ${contacts.length} contacts from ${csvPath}`);
          resolve(contacts);
        })
        .on('error', (error) => {
          LoggerService.error('Failed to parse CSV file', { error });
          reject(error);
        });
    });
  }

  async validateTemplate(contacts: Contact[], templateName: string): Promise<boolean> {
    try {
      // Check for required fields in template
      const requiredVariables = ['name', 'email', 'unsubscribe_url'];
      const missing = this.templateEngine.validateTemplate(templateName, requiredVariables);

      if (missing.length > 0) {
        LoggerService.warn('Template missing required placeholders', { missing });
      }

      // Test render with first contact
      if (contacts.length > 0) {
        const testVariables = this.emailComposer.createPersonalizedVariables(contacts[0]);
        const rendered = this.templateEngine.render(templateName, testVariables);

        if (!rendered.includes(contacts[0].email)) {
          LoggerService.warn('Template rendering may not be working correctly');
        }
      }

      return true;
    } catch (error) {
      LoggerService.error('Template validation failed', { error });
      return false;
    }
  }

  async runCampaign(
    templateName: string = 'newsletter',
    batchSize: number = config.campaign.batchSize,
    delayMs: number = config.campaign.delayMs
  ): Promise<CampaignStats> {
    this.stats.startTime = new Date();
    LoggerService.campaignStarted(0);

    try {
      // Load contacts
      const contacts = await this.loadContacts();
      this.stats.total = contacts.length;

      // Validate template
      await this.validateTemplate(contacts, templateName);

      // Test email connection
      const connectionOk = await this.emailService.testConnection();
      if (!connectionOk) {
        throw new Error('SMTP connection failed');
      }

      // Process in batches
      const batches = this.createBatches(contacts, batchSize);

      for (let i = 0; i < batches.length; i++) {
        await this.processBatch(batches[i], i + 1, batches.length, templateName, delayMs);
      }

      // Generate reports
      await this.generateReports();

    } catch (error) {
      LoggerService.error('Campaign failed', { error: error instanceof Error ? error.message : String(error) });
    } finally {
      this.stats.endTime = new Date();
      this.stats.durationMs = this.stats.endTime.getTime() - this.stats.startTime.getTime();

      // Close email service
      await this.emailService.close();

      // Log completion
      LoggerService.campaignCompleted(this.stats);
    }

    return this.stats;
  }

  private createBatches(contacts: Contact[], batchSize: number): Contact[][] {
    const batches: Contact[][] = [];

    for (let i = 0; i < contacts.length; i += batchSize) {
      batches.push(contacts.slice(i, i + batchSize));
    }

    LoggerService.info(`Created ${batches.length} batches of ${batchSize} contacts each`);
    return batches;
  }

  private async processBatch(
    contacts: Contact[],
    batchNumber: number,
    totalBatches: number,
    templateName: string,
    delayMs: number
  ): Promise<void> {
    LoggerService.info(`Processing batch ${batchNumber}/${totalBatches}`, {
      batchSize: contacts.length,
    });

    // Create email options for all contacts in batch
    const { valid: emailOptions, invalid } = this.emailComposer.batchCreateEmailOptions(
      contacts,
      templateName
    );

    // Update stats for invalid contacts
    this.invalidContacts.push(...invalid);
    this.stats.invalid += invalid.length;

    // Send emails
    const results = await this.emailService.sendBatch(
      emailOptions,
      delayMs,
      (progress) => {
        this.logProgress(batchNumber, totalBatches, progress);
      }
    );

    // Update results and stats
    this.results.push(...results);
    results.forEach(result => {
      if (result.success) {
        this.stats.sent++;
      } else {
        this.stats.failed++;
      }
    });

    LoggerService.info(`Batch ${batchNumber} completed`, {
      sent: results.filter(r => r.success).length,
      failed: results.filter(r => !r.success).length,
      invalid: invalid.length,
    });
  }

  private logProgress(
    batchNumber: number,
    totalBatches: number,
    progress: any
  ): void {
    const overallProgress = Math.round(
      ((batchNumber - 1) / totalBatches) * 100 + 
      (progress.percentage / totalBatches)
    );

    LoggerService.debug(`Progress: ${overallProgress}%`, {
      batch: `${batchNumber}/${totalBatches}`,
      batchProgress: `${progress.current}/${progress.total}`,
      overallProgress: `${overallProgress}%`,
    });
  }

  private async generateReports(): Promise<void> {
    const reportDir = path.join(config.paths.logDir, 'reports');
    if (!fs.existsSync(reportDir)) {
      fs.mkdirSync(reportDir, { recursive: true });
    }

    // Generate CSV report
    await this.generateCsvReport(reportDir);

    // Generate JSON summary
    await this.generateJsonSummary(reportDir);

    // Generate HTML report
    await this.generateHtmlReport(reportDir);
  }

  private async generateCsvReport(reportDir: string): Promise<void> {
    const reportPath = path.join(
      reportDir,
      `campaign_report_${new Date().toISOString().replace(/[:.]/g, '-')}.csv`
    );

    const csvData = [
      ['Timestamp', 'Email', 'Name', 'Status', 'Message ID', 'Error', 'Retry Count'],
      ...this.results.map(result => [
        result.timestamp.toISOString(),
        result.contact.email,
        result.contact.name,
        result.success ? 'SUCCESS' : 'FAILED',
        result.messageId || '',
        result.error || '',
        result.retryCount,
      ]),
    ];

    const csvContent = csvData.map(row => row.join(',')).join('\n');
    fs.writeFileSync(reportPath, csvContent, 'utf-8');

    LoggerService.info(`CSV report generated: ${reportPath}`);
  }

  private async generateJsonSummary(reportDir: string): Promise<void> {
    const summaryPath = path.join(
      reportDir,
      `campaign_summary_${new Date().toISOString().replace(/[:.]/g, '-')}.json`
    );

    const summary = {
      campaign: config.campaign.name,
      stats: this.stats,
      timestamp: new Date().toISOString(),
      duration: `${(this.stats.durationMs / 1000).toFixed(2)} seconds`,
      successRate: this.stats.total > 0 
        ? ((this.stats.sent / this.stats.total) * 100).toFixed(2) + '%'
        : '0%',
    };

    fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf-8');
    LoggerService.info(`JSON summary generated: ${summaryPath}`);
  }

  private async generateHtmlReport(reportDir: string): Promise<void> {
    const reportPath = path.join(
      reportDir,
      `campaign_report_${new Date().toISOString().replace(/[:.]/g, '-')}.html`
    );

    const htmlContent = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Campaign Report - ${config.campaign.name}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
        .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0; }
        .stat-card { background: #f8f9fa; padding: 20px; border-radius: 5px; border-left: 4px solid #3498db; }
        .stat-value { font-size: 2em; font-weight: bold; margin: 10px 0; }
        .success { color: #27ae60; }
        .failed { color: #e74c3c; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background-color: #f2f2f2; }
        tr.success-row { background-color: #d4edda; }
        tr.failed-row { background-color: #f8d7da; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Campaign Report: ${config.campaign.name}</h1>
        <p>Generated on ${new Date().toLocaleString()}</p>
    </div>

    <div class="stats">
        <div class="stat-card">
            <div>Total Contacts</div>
            <div class="stat-value">${this.stats.total}</div>
        </div>
        <div class="stat-card">
            <div>Successfully Sent</div>
            <div class="stat-value success">${this.stats.sent}</div>
        </div>
        <div class="stat-card">
            <div>Failed</div>
            <div class="stat-value failed">${this.stats.failed}</div>
        </div>
        <div class="stat-card">
            <div>Success Rate</div>
            <div class="stat-value">
                ${this.stats.total > 0 ? ((this.stats.sent / this.stats.total) * 100).toFixed(2) + '%' : '0%'}
            </div>
        </div>
    </div>

    <h2>Detailed Results</h2>
    <table>
        <thead>
            <tr>
                <th>Timestamp</th>
                <th>Email</th>
                <th>Name</th>
                <th>Status</th>
                <th>Message ID</th>
                <th>Error</th>
            </tr>
        </thead>
        <tbody>
            ${this.results.map(result => `
            <tr class="${result.success ? 'success-row' : 'failed-row'}">
                <td>${result.timestamp.toLocaleString()}</td>
                <td>${result.contact.email}</td>
                <td>${result.contact.name}</td>
                <td>${result.success ? '✅ SUCCESS' : '❌ FAILED'}</td>
                <td>${result.messageId || '-'}</td>
                <td>${result.error || '-'}</td>
            </tr>
            `).join('')}
        </tbody>
    </table>
</body>
</html>
    `;

    fs.writeFileSync(reportPath, htmlContent, 'utf-8');
    LoggerService.info(`HTML report generated: ${reportPath}`);
  }

  getResults(): SendResult[] {
    return this.results;
  }

  getInvalidContacts(): Contact[] {
    return this.invalidContacts;
  }

  getCampaignStats(): CampaignStats {
    return { ...this.stats };
  }
}

Storage Service (Optional for Advanced Features)

src/services/StorageService.ts

import fs from 'fs';
import path from 'path';
import { LoggerService } from './LoggerService';
import { config } from '../config';
import { Contact, SendResult } from '../core/types';

export class StorageService {
  private storagePath: string;

  constructor(storagePath: string = './storage') {
    this.storagePath = storagePath;
    this.ensureDirectory();
  }

  private ensureDirectory(): void {
    if (!fs.existsSync(this.storagePath)) {
      fs.mkdirSync(this.storagePath, { recursive: true });
    }
  }

  async saveContacts(contacts: Contact[], filename: string): Promise<void> {
    const filePath = path.join(this.storagePath, filename);

    // Convert to CSV
    const csvContent = [
      Object.keys(contacts[0] || {}),
      ...contacts.map(contact => Object.values(contact))
    ].map(row => row.join(',')).join('\n');

    fs.writeFileSync(filePath, csvContent, 'utf-8');
    LoggerService.info(`Contacts saved to ${filePath}`);
  }

  async saveResults(results: SendResult[], campaignName: string): Promise<void> {
    const filePath = path.join(
      this.storagePath,
      `results_${campaignName}_${Date.now()}.json`
    );

    fs.writeFileSync(
      filePath,
      JSON.stringify(results, null, 2),
      'utf-8'
    );

    LoggerService.info(`Results saved to ${filePath}`);
  }

  async loadPreviousResults(campaignName: string): Promise<SendResult[]> {
    const files = fs.readdirSync(this.storagePath);
    const resultFiles = files.filter(file => 
      file.startsWith(`results_${campaignName}_`)
    ).sort().reverse();

    if (resultFiles.length === 0) {
      return [];
    }

    const latestFile = resultFiles[0];
    const filePath = path.join(this.storagePath, latestFile);

    const content = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(content);
  }

  async archiveCampaign(campaignName: string, data: any): Promise<void> {
    const archivePath = path.join(
      this.storagePath,
      'archive',
      campaignName
    );

    if (!fs.existsSync(archivePath)) {
      fs.mkdirSync(archivePath, { recursive: true });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const archiveFile = path.join(archivePath, `archive_${timestamp}.json`);

    fs.writeFileSync(
      archiveFile,
      JSON.stringify(data, null, 2),
      'utf-8'
    );

    LoggerService.info(`Campaign archived to ${archiveFile}`);
  }
}

Main Application Entry Point

src/index.ts

import { config } from './config';
import { LoggerService } from './services/LoggerService';
import { TemplateEngine } from './core/TemplateEngine';
import { EmailComposer } from './core/EmailComposer';
import { EmailService } from './services/EmailService';
import { CampaignEngine } from './core/CampaignEngine';
import { StorageService } from './services/StorageService';

async function main() {
  try {
    LoggerService.info('Starting email campaign system...');
    LoggerService.info(`Campaign: ${config.campaign.name}`);
    LoggerService.info(`Environment: ${process.env.NODE_ENV || 'development'}`);

    // Initialize services
    const templateEngine = new TemplateEngine();
    const emailService = new EmailService();
    const emailComposer = new EmailComposer(templateEngine);
    const storageService = new StorageService();
    const campaignEngine = new CampaignEngine(
      emailComposer,
      emailService,
      templateEngine
    );

    // List available templates
    const templates = templateEngine.listTemplates();
    LoggerService.info(`Available templates: ${templates.join(', ')}`);

    if (templates.length === 0) {
      LoggerService.warn('No templates found. Creating default template...');
      templateEngine.addTemplate('newsletter', createDefaultTemplate());
    }

    // Run campaign
    const stats = await campaignEngine.runCampaign(
      'newsletter',
      config.campaign.batchSize,
      config.campaign.delayMs
    );

    // Archive results
    const results = campaignEngine.getResults();
    await storageService.saveResults(results, config.campaign.name);
    await storageService.archiveCampaign(config.campaign.name, {
      stats,
      config,
      timestamp: new Date().toISOString(),
    });

    // Print summary
    printSummary(stats);

    process.exit(0);

  } catch (error) {
    LoggerService.error('Fatal error in main application', {
      error: error instanceof Error ? error.message : String(error),
      stack: error instanceof Error ? error.stack : undefined,
    });
    process.exit(1);
  }
}

function createDefaultTemplate(): string {
  return `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Update from {company}</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background: #2c3e50; color: white; padding: 30px; text-align: center; }
        .content { background: #f8f9fa; padding: 30px; }
        .footer { background: #e9ecef; padding: 20px; text-align: center; font-size: 12px; color: #6c757d; }
        h1 { margin-top: 0; }
        .button { display: inline-block; background: #3498db; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin: 15px 0; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Hello {first_name},</h1>
        </div>
        <div class="content">
            <p>We're excited to share our latest updates with you.</p>
            <p>This email was sent as part of our {campaign_name} campaign.</p>
            <a href="https://yourdomain.com" class="button">Learn More</a>
        </div>
        <div class="footer">
            <p>&copy; {current_year} Your Company. All rights reserved.</p>
            <p>
                <a href="{unsubscribe_url}">Unsubscribe</a> | 
                <a href="https://yourdomain.com/preferences">Update Preferences</a>
            </p>
        </div>
    </div>
</body>
</html>
  `;
}

function printSummary(stats: any): void {
  console.log('\n' + '='.repeat(60));
  console.log('CAMPAIGN SUMMARY');
  console.log('='.repeat(60));
  console.log(`Total Contacts: ${stats.total}`);
  console.log(`Successfully Sent: ${stats.sent}`);
  console.log(`Failed: ${stats.failed}`);
  console.log(`Invalid/Skipped: ${stats.invalid + stats.skipped}`);

  if (stats.total > 0) {
    const successRate = (stats.sent / stats.total) * 100;
    console.log(`Success Rate: ${successRate.toFixed(2)}%`);
  }

  console.log(`Duration: ${(stats.durationMs / 1000).toFixed(2)} seconds`);
  console.log('='.repeat(60));
  console.log('Check the logs directory for detailed reports.');
  console.log('='.repeat(60));
}

// Handle graceful shutdown
process.on('SIGINT', () => {
  LoggerService.info('Received SIGINT. Shutting down gracefully...');
  process.exit(0);
});

process.on('SIGTERM', () => {
  LoggerService.info('Received SIGTERM. Shutting down gracefully...');
  process.exit(0);
});

// Run application
if (require.main === module) {
  main();
}

export { main };

Sample Template File

src/templates/newsletter.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Important Updates for {company}</title>
    <style>
        /* Reset and base styles */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            line-height: 1.6;
            color: #333333;
            background-color: #f5f5f5;
            margin: 0;
            padding: 0;
        }

        .email-container {
            max-width: 600px;
            margin: 0 auto;
            background-color: #ffffff;
        }

        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 40px 30px;
            text-align: center;
        }

        .header h1 {
            font-size: 28px;
            font-weight: 600;
            margin-bottom: 10px;
        }

        .header p {
            font-size: 16px;
            opacity: 0.9;
        }

        .content {
            padding: 40px 30px;
        }

        .greeting {
            font-size: 18px;
            margin-bottom: 30px;
            color: #2d3748;
        }

        .section {
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 1px solid #e2e8f0;
        }

        .section:last-child {
            border-bottom: none;
        }

        .section-title {
            font-size: 20px;
            font-weight: 600;
            color: #4a5568;
            margin-bottom: 15px;
        }

        .personalized-highlight {
            background-color: #ebf8ff;
            border-left: 4px solid #4299e1;
            padding: 20px;
            margin: 20px 0;
            border-radius: 0 4px 4px 0;
        }

        .personalized-highlight h3 {
            color: #2b6cb0;
            margin-bottom: 10px;
        }

        .cta-button {
            display: inline-block;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 14px 28px;
            text-decoration: none;
            border-radius: 6px;
            font-weight: 500;
            text-align: center;
            margin: 20px 0;
            transition: transform 0.2s, box-shadow 0.2s;
        }

        .cta-button:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
        }

        .features {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin: 30px 0;
        }

        .feature {
            background: #f7fafc;
            padding: 20px;
            border-radius: 6px;
            border: 1px solid #e2e8f0;
        }

        .feature h4 {
            color: #4a5568;
            margin-bottom: 10px;
        }

        .signature {
            margin-top: 40px;
            padding-top: 20px;
            border-top: 1px solid #e2e8f0;
        }

        .signature p {
            margin-bottom: 5px;
        }

        .footer {
            background-color: #2d3748;
            color: #cbd5e0;
            padding: 30px;
            text-align: center;
            font-size: 14px;
        }

        .footer-links {
            margin: 20px 0;
        }

        .footer-links a {
            color: #cbd5e0;
            text-decoration: none;
            margin: 0 10px;
            transition: color 0.2s;
        }

        .footer-links a:hover {
            color: #ffffff;
        }

        .tracking-pixel {
            display: none;
        }

        /* Responsive adjustments */
        @media (max-width: 600px) {
            .header, .content, .footer {
                padding: 20px 15px;
            }

            .features {
                grid-template-columns: 1fr;
            }

            .header h1 {
                font-size: 24px;
            }
        }
    </style>
</head>
<body>
    <div class="email-container">
        <!-- Tracking pixel (invisible) -->
        <img src="{tracking_pixel}" alt="" width="1" height="1" class="tracking-pixel" />

        <!-- Header -->
        <div class="header">
            <h1>Hello {first_name},</h1>
            <p>Here's what's new for {company} this month</p>
        </div>

        <!-- Main Content -->
        <div class="content">
            <div class="greeting">
                <p>Dear {name},</p>
                <p>We hope this message finds you well. Here are the latest updates and insights specifically curated for {company}.</p>
            </div>

            <!-- Personalized Section -->
            <div class="personalized-highlight">
                <h3>✨ Personalized Insights for {first_name}</h3>
                <p>Based on your activity in the {industry} sector, we've identified key opportunities that could drive growth for {company}.</p>
                <p>Our data shows that similar companies have achieved an average 28% increase in engagement after implementing these strategies.</p>
            </div>

            <!-- Features/Updates -->
            <div class="section">
                <h2 class="section-title">Latest Updates</h2>
                <div class="features">
                    <div class="feature">
                        <h4>Industry Analysis</h4>
                        <p>Comprehensive report on {industry} trends and predictions for the coming quarter.</p>
                    </div>
                    <div class="feature">
                        <h4>Best Practices</h4>
                        <p>Case studies from leading companies in your sector.</p>
                    </div>
                    <div class="feature">
                        <h4>Tools & Resources</h4>
                        <p>New tools specifically designed for {industry} professionals.</p>
                    </div>
                </div>
            </div>

            <!-- Call to Action -->
            <div class="section">
                <h2 class="section-title">Take the Next Step</h2>
                <p>Ready to implement these strategies for {company}?</p>
                <a href="https://yourdomain.com/dashboard?tracking={tracking_id}" class="cta-button">
                    Access Your Custom Dashboard
                </a>
                <p style="font-size: 14px; color: #718096;">
                    This link is unique to you and will track your progress.
                </p>
            </div>

            <!-- Signature -->
            <div class="signature">
                <p>Best regards,</p>
                <p><strong>[Your Name]</strong></p>
                <p>[Your Position]</p>
                <p>[Your Company]</p>
                <p>Email: [email protected]</p>
                <p>Phone: (123) 456-7890</p>
            </div>
        </div>

        <!-- Footer -->
        <div class="footer">
            <p>&copy; {current_year} Your Company. All rights reserved.</p>
            <p>{company} | {email}</p>

            <div class="footer-links">
                <a href="{unsubscribe_url}">Unsubscribe</a> |
                <a href="https://yourdomain.com/preferences">Update Preferences</a> |
                <a href="https://yourdomain.com/privacy">Privacy Policy</a> |
                <a href="https://yourdomain.com/terms">Terms of Service</a>
            </div>

            <p style="font-size: 12px; margin-top: 20px; color: #a0aec0;">
                You're receiving this email as part of our {campaign_name} campaign.<br>
                Our mailing address is: 123 Business Ave, Suite 100, City, State 12345
            </p>
        </div>
    </div>
</body>
</html>

Sample CSV Data

data/contacts.csv

email,name,company,industry,subscription_date,last_engagement,preferred_category
[email protected],Alex Johnson,TechInnovate Inc.,SaaS,2023-12-15,2024-02-10,Product Updates
[email protected],Sarah Chen,Design Studio,Creative Services,2023-11-20,2024-02-15,Design Resources
[email protected],Michael Rodriguez,Retail Solutions,Retail,2024-01-10,2024-02-05,Marketing Tips
[email protected],Priya Sharma,HealthTech Labs,Healthcare,2023-10-05,2024-02-12,Industry News
[email protected],David Wilson,FinTech Group,Finance,2024-02-01,2024-02-18,Technical Guides
[email protected],Lisa Martin,Consulting Pro,Consulting,2023-09-15,2024-02-08,Case Studies
[email protected],Robert Kim,Startup Alpha,Technology,2024-01-22,2024-02-14,Startup Advice
[email protected],Emily Carter,Green Energy Corp,Renewable Energy,2023-08-30,2024-02-03,Sustainability
[email protected],James Lee,LegalTech Solutions,Legal Tech,2024-02-10,2024-02-16,Compliance Updates
[email protected],Maria Garcia,EduTech Innovations,Education,2023-12-01,2024-02-09,Learning Resources

Package Configuration

package.json

{
  "name": "email-campaign-system",
  "version": "1.0.0",
  "description": "Advanced email campaign system with personalization using Node.js and TypeScript",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "concurrently \"npx tsc --watch\" \"nodemon dist/index.js\"",
    "campaign": "ts-node src/index.ts",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "format": "prettier --write src/**/*.ts",
    "prepare": "npm run build"
  },
  "keywords": [
    "email",
    "campaign",
    "newsletter",
    "typescript",
    "nodejs",
    "smtp",
    "personalization"
  ],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "nodemailer": "^6.9.7",
    "csv-parser": "^3.0.0",
    "dotenv": "^16.3.1",
    "winston": "^3.11.0"
  },
  "devDependencies": {
    "@types/node": "^20.8.0",
    "@types/nodemailer": "^6.4.14",
    "@types/csv-parser": "^3.0.3",
    "typescript": "^5.2.2",
    "ts-node": "^10.9.1",
    "nodemon": "^3.0.1",
    "concurrently": "^8.2.1",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "@typescript-eslint/parser": "^6.7.4",
    "eslint": "^8.51.0",
    "prettier": "^3.0.3"
  },
  "engines": {
    "node": ">=16.0.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Running the Campaign

Quick Start

# 1. Clone or create the project structure
mkdir email-campaign && cd email-campaign

# 2. Copy all the files above into their respective directories

# 3. Install dependencies
npm install

# 4. Set up environment variables
cp .env.example .env
# Edit .env with your SMTP credentials

# 5. Prepare your data
# Create data/contacts.csv with your recipient list
# Create src/templates/newsletter.html with your email template

# 6. Run the campaign
npm run campaign

# Or for development with auto-reload
npm run dev

Command Line Arguments (Enhanced)

// Add this to src/index.ts for CLI support
import { Command } from 'commander';

const program = new Command();

program
  .name('email-campaign')
  .description('CLI tool for managing email campaigns')
  .version('1.0.0');

program
  .command('send')
  .description('Send a new email campaign')
  .option('-t, --template <name>', 'Template name', 'newsletter')
  .option('-c, --contacts <path>', 'Contacts CSV file path')
  .option('-b, --batch-size <number>', 'Batch size', '50')
  .option('-d, --delay <ms>', 'Delay between emails in ms', '2000')
  .action(async (options) => {
    // Override config with CLI options
    if (options.contacts) {
      config.paths.contactsCsv = options.contacts;
    }

    // Run campaign with options
    const campaignEngine = new CampaignEngine(
      new EmailComposer(new TemplateEngine()),
      new EmailService(),
      new TemplateEngine()
    );

    await campaignEngine.runCampaign(
      options.template,
      parseInt(options.batchSize),
      parseInt(options.delay)
    );
  });

program
  .command('list-templates')
  .description('List all available templates')
  .action(() => {
    const templateEngine = new TemplateEngine();
    console.log('Available templates:');
    templateEngine.listTemplates().forEach(template => {
      console.log(`  - ${template}`);
    });
  });

program
  .command('validate <template>')
  .description('Validate a template file')
  .action((templateName) => {
    const templateEngine = new TemplateEngine();
    const requiredVars = ['name', 'email', 'unsubscribe_url'];
    const missing = templateEngine.validateTemplate(templateName, requiredVars);

    if (missing.length === 0) {
      console.log(`✅ Template "${templateName}" is valid`);
    } else {
      console.log(`❌ Template "${templateName}" is missing: ${missing.join(', ')}`);
    }
  });

program.parse();

Key Features of This Implementation

1. Type Safety

  • Full TypeScript support with strict typing
  • Interface definitions for all data structures
  • Compile-time error checking

2. Performance Optimizations

  • Non-blocking I/O operations
  • Connection pooling for SMTP
  • Batch processing with configurable sizes
  • Parallel sending capabilities

3. Robust Error Handling

  • Exponential backoff for retries
  • Comprehensive logging at multiple levels
  • Graceful shutdown handling
  • Detailed error reporting

4. Advanced Features

  • Template engine with conditional logic
  • HTML to plain text conversion
  • Tracking pixel and unsubscribe link generation
  • Campaign statistics and analytics
  • Multiple report formats (CSV, JSON, HTML)

5. Security

  • Environment variables for credentials
  • Secure unsubscribe token generation
  • Email validation and sanitization
  • Rate limiting to prevent spam flags

6. Scalability

  • Modular architecture
  • Easy to extend with new features
  • Configurable batch sizes and delays
  • Support for large contact lists

Best Practices Implemented

  1. Separation of Concerns: Each class has a single responsibility
  2. Dependency Injection: Components are loosely coupled
  3. Configuration Management: Centralized config with environment support
  4. Comprehensive Logging: Structured logs with multiple transports
  5. Error Boundaries: Graceful degradation when errors occur
  6. Resource Management: Proper cleanup of connections and files
  7. Performance Monitoring: Built-in timing and statistics

Production Considerations

Deployment

# Build for production
npm run build

# Run in production
NODE_ENV=production node dist/index.js

# Or use PM2 for process management
npm install -g pm2
pm2 start dist/index.js --name "email-campaign"
pm2 save
pm2 startup

Monitoring

  • Use Winston’s file transport for log rotation
  • Integrate with monitoring services (Datadog, New Relic)
  • Set up alerts for failed email rates
  • Monitor SMTP connection health

Scaling

  • Use a message queue (RabbitMQ, Redis) for very large campaigns
  • Implement database storage instead of CSV for large datasets
  • Consider using a dedicated email sending service (SendGrid, Amazon SES) for high volumes

Conclusion

This Node.js/TypeScript implementation provides a robust, scalable, and production-ready email campaign system. It offers significant advantages over the Python version:

  1. Better Performance: Non-blocking I/O handles large volumes efficiently
  2. Modern Tooling: Access to the rich npm ecosystem
  3. Type Safety: Catch errors at compile time
  4. Real-time Processing: Better for streaming and concurrent operations
  5. Enterprise Ready: Suitable for integration into larger applications

The system is modular, extensible, and follows modern software engineering practices. You can easily add features like A/B testing, advanced analytics, webhook integration, or a REST API for campaign management.

Remember to:

  • Always test with small batches first
  • Monitor your email provider’s sending limits
  • Respect unsubscribe requests promptly
  • Keep your contact list clean and updated
  • Comply with email regulations (CAN-SPAM, GDPR)

With this system, you have a powerful tool for personalized email communication that can scale from small newsletters to enterprise-level campaigns.

Posts Carousel

Leave a Comment

Your email address will not be published. Required fields are marked with *

Latest Posts

Most Commented

Featured Videos