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?
- Non-blocking I/O: Send multiple emails concurrently without waiting for each to complete
- Real-time Processing: Better for handling large batches and streaming data
- Type Safety: TypeScript catches errors at compile time, not runtime
- Modern Ecosystem: Access to thousands of specialized npm packages
- 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(/ /g, ' ')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/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>© {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>© {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
- Separation of Concerns: Each class has a single responsibility
- Dependency Injection: Components are loosely coupled
- Configuration Management: Centralized config with environment support
- Comprehensive Logging: Structured logs with multiple transports
- Error Boundaries: Graceful degradation when errors occur
- Resource Management: Proper cleanup of connections and files
- 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:
- Better Performance: Non-blocking I/O handles large volumes efficiently
- Modern Tooling: Access to the rich npm ecosystem
- Type Safety: Catch errors at compile time
- Real-time Processing: Better for streaming and concurrent operations
- 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.

























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