platformservice/ts/deliverability/classes.ipwarmupmanager.ts

896 lines
28 KiB
TypeScript
Raw Normal View History

2025-05-07 20:20:17 +00:00
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { LRUCache } from 'lru-cache';
/**
* Represents a single stage in the warmup process
*/
export interface IWarmupStage {
/** Stage number (1-based) */
stage: number;
/** Maximum daily email volume for this stage */
maxDailyVolume: number;
/** Duration of this stage in days */
durationDays: number;
/** Target engagement metrics for this stage */
targetMetrics?: {
/** Minimum open rate (percentage) */
minOpenRate?: number;
/** Maximum bounce rate (percentage) */
maxBounceRate?: number;
/** Maximum spam complaint rate (percentage) */
maxComplaintRate?: number;
};
}
/**
* Configuration for IP warmup process
*/
export interface IIPWarmupConfig {
/** Whether the warmup is enabled */
enabled?: boolean;
/** List of IP addresses to warm up */
ipAddresses?: string[];
/** Target domains to warm up (e.g. your sending domains) */
targetDomains?: string[];
/** Warmup stages defining volume and duration */
stages?: IWarmupStage[];
/** Date when warmup process started */
startDate?: Date;
/** Default hourly distribution for sending (percentage of daily volume per hour) */
hourlyDistribution?: number[];
/** Whether to automatically advance stages based on metrics */
autoAdvanceStages?: boolean;
/** Whether to suspend warmup if metrics decline */
suspendOnMetricDecline?: boolean;
/** Percentage of traffic to send through fallback provider during warmup */
fallbackPercentage?: number;
/** Whether to prioritize engaged subscribers during warmup */
prioritizeEngagedSubscribers?: boolean;
}
/**
* Status for a specific IP's warmup process
*/
export interface IIPWarmupStatus {
/** IP address being warmed up */
ipAddress: string;
/** Current warmup stage */
currentStage: number;
/** Start date of the warmup process */
startDate: Date;
/** Start date of the current stage */
currentStageStartDate: Date;
/** Target completion date for entire warmup */
targetCompletionDate: Date;
/** Daily volume allocation for current stage */
currentDailyAllocation: number;
/** Emails sent in current stage */
sentInCurrentStage: number;
/** Total emails sent during warmup process */
totalSent: number;
/** Whether the warmup is currently active */
isActive: boolean;
/** Daily statistics for the past week */
dailyStats: Array<{
/** Date of the statistics */
date: string;
/** Number of emails sent */
sent: number;
/** Number of emails opened */
opened: number;
/** Number of bounces */
bounces: number;
/** Number of spam complaints */
complaints: number;
}>;
/** Current metrics */
metrics: {
/** Open rate percentage */
openRate: number;
/** Bounce rate percentage */
bounceRate: number;
/** Complaint rate percentage */
complaintRate: number;
};
}
/**
* Defines methods for a policy used to allocate emails to different IPs
*/
export interface IIPAllocationPolicy {
/** Name of the policy */
name: string;
/**
* Allocate an IP address for sending an email
* @param availableIPs List of available IP addresses
* @param emailInfo Information about the email being sent
* @returns The IP to use, or null if no IP is available
*/
allocateIP(
availableIPs: Array<{ ip: string; priority: number; capacity: number }>,
emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional: boolean;
isWarmup: boolean;
}
): string | null;
}
/**
* Default IP warmup configuration with industry standard stages
*/
const DEFAULT_WARMUP_CONFIG: Required<IIPWarmupConfig> = {
enabled: true,
ipAddresses: [],
targetDomains: [],
stages: [
{ stage: 1, maxDailyVolume: 50, durationDays: 2, targetMetrics: { maxBounceRate: 8, minOpenRate: 15 } },
{ stage: 2, maxDailyVolume: 100, durationDays: 2, targetMetrics: { maxBounceRate: 7, minOpenRate: 18 } },
{ stage: 3, maxDailyVolume: 500, durationDays: 3, targetMetrics: { maxBounceRate: 6, minOpenRate: 20 } },
{ stage: 4, maxDailyVolume: 1000, durationDays: 3, targetMetrics: { maxBounceRate: 5, minOpenRate: 20 } },
{ stage: 5, maxDailyVolume: 5000, durationDays: 5, targetMetrics: { maxBounceRate: 3, minOpenRate: 22 } },
{ stage: 6, maxDailyVolume: 10000, durationDays: 5, targetMetrics: { maxBounceRate: 2, minOpenRate: 25 } },
{ stage: 7, maxDailyVolume: 20000, durationDays: 5, targetMetrics: { maxBounceRate: 1, minOpenRate: 25 } },
{ stage: 8, maxDailyVolume: 50000, durationDays: 5, targetMetrics: { maxBounceRate: 1, minOpenRate: 25 } },
],
startDate: new Date(),
// Default hourly distribution (percentage per hour, sums to 100%)
hourlyDistribution: [
1, 1, 1, 1, 1, 2, 3, 5, 7, 8, 10, 11,
10, 9, 8, 6, 5, 4, 3, 2, 1, 1, 1, 0
],
autoAdvanceStages: true,
suspendOnMetricDecline: true,
fallbackPercentage: 50,
prioritizeEngagedSubscribers: true
};
/**
* Manages the IP warming process for new sending IPs
*/
export class IPWarmupManager {
private static instance: IPWarmupManager;
private config: Required<IIPWarmupConfig>;
private warmupStatuses: Map<string, IIPWarmupStatus> = new Map();
private dailySendCounts: Map<string, number> = new Map();
private hourlySendCounts: Map<string, number[]> = new Map();
private isInitialized: boolean = false;
private allocationPolicies: Map<string, IIPAllocationPolicy> = new Map();
private activePolicy: string = 'balanced';
/**
* Constructor for IPWarmupManager
* @param config Warmup configuration
*/
constructor(config: IIPWarmupConfig = {}) {
this.config = {
...DEFAULT_WARMUP_CONFIG,
...config,
stages: config.stages || [...DEFAULT_WARMUP_CONFIG.stages]
};
// Register default allocation policies
this.registerAllocationPolicy('balanced', new BalancedAllocationPolicy());
this.registerAllocationPolicy('roundRobin', new RoundRobinAllocationPolicy());
this.registerAllocationPolicy('dedicated', new DedicatedDomainPolicy());
this.initialize();
}
/**
* Get the singleton instance of IPWarmupManager
* @param config Warmup configuration
* @returns Singleton instance
*/
public static getInstance(config: IIPWarmupConfig = {}): IPWarmupManager {
if (!IPWarmupManager.instance) {
IPWarmupManager.instance = new IPWarmupManager(config);
}
return IPWarmupManager.instance;
}
/**
* Initialize the warmup manager
*/
private initialize(): void {
if (this.isInitialized) return;
try {
// Load warmup statuses from storage
this.loadWarmupStatuses();
// Initialize any new IPs that might have been added to config
for (const ip of this.config.ipAddresses) {
if (!this.warmupStatuses.has(ip)) {
this.initializeIPWarmup(ip);
}
}
// Initialize daily and hourly counters
const today = new Date().toISOString().split('T')[0];
for (const ip of this.config.ipAddresses) {
this.dailySendCounts.set(ip, 0);
this.hourlySendCounts.set(ip, Array(24).fill(0));
}
// Schedule daily reset of counters
this.scheduleDailyReset();
// Schedule daily evaluation of warmup progress
this.scheduleDailyEvaluation();
this.isInitialized = true;
logger.log('info', `IP Warmup Manager initialized with ${this.config.ipAddresses.length} IPs`);
} catch (error) {
logger.log('error', `Failed to initialize IP Warmup Manager: ${error.message}`, {
stack: error.stack
});
}
}
/**
* Initialize warmup status for a new IP address
* @param ipAddress IP address to initialize
*/
private initializeIPWarmup(ipAddress: string): void {
const startDate = new Date();
let targetCompletionDate = new Date(startDate);
// Calculate target completion date based on stages
let totalDays = 0;
for (const stage of this.config.stages) {
totalDays += stage.durationDays;
}
targetCompletionDate.setDate(targetCompletionDate.getDate() + totalDays);
const warmupStatus: IIPWarmupStatus = {
ipAddress,
currentStage: 1,
startDate,
currentStageStartDate: new Date(),
targetCompletionDate,
currentDailyAllocation: this.config.stages[0].maxDailyVolume,
sentInCurrentStage: 0,
totalSent: 0,
isActive: true,
dailyStats: [],
metrics: {
openRate: 0,
bounceRate: 0,
complaintRate: 0
}
};
this.warmupStatuses.set(ipAddress, warmupStatus);
this.saveWarmupStatuses();
logger.log('info', `Initialized warmup for IP ${ipAddress}`, {
currentStage: 1,
targetCompletion: targetCompletionDate.toISOString().split('T')[0]
});
}
/**
* Schedule daily reset of send counters
*/
private scheduleDailyReset(): void {
// Calculate time until midnight
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const timeUntilMidnight = tomorrow.getTime() - now.getTime();
// Schedule reset
setTimeout(() => {
this.resetDailyCounts();
// Reschedule for next day
this.scheduleDailyReset();
}, timeUntilMidnight);
logger.log('info', `Scheduled daily counter reset in ${Math.floor(timeUntilMidnight / 60000)} minutes`);
}
/**
* Reset daily send counters
*/
private resetDailyCounts(): void {
for (const ip of this.config.ipAddresses) {
// Save yesterday's count to history before resetting
const status = this.warmupStatuses.get(ip);
if (status) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
// Update daily stats with yesterday's data
const sentCount = this.dailySendCounts.get(ip) || 0;
status.dailyStats.push({
date: yesterday.toISOString().split('T')[0],
sent: sentCount,
opened: Math.floor(sentCount * status.metrics.openRate / 100),
bounces: Math.floor(sentCount * status.metrics.bounceRate / 100),
complaints: Math.floor(sentCount * status.metrics.complaintRate / 100)
});
// Keep only the last 7 days of stats
if (status.dailyStats.length > 7) {
status.dailyStats.shift();
}
}
// Reset counters for today
this.dailySendCounts.set(ip, 0);
this.hourlySendCounts.set(ip, Array(24).fill(0));
}
// Save updated statuses
this.saveWarmupStatuses();
logger.log('info', 'Daily send counters reset');
}
/**
* Schedule daily evaluation of warmup progress
*/
private scheduleDailyEvaluation(): void {
// Calculate time until 1 AM (do evaluation after midnight)
const now = new Date();
const evaluationTime = new Date(now);
evaluationTime.setDate(evaluationTime.getDate() + 1);
evaluationTime.setHours(1, 0, 0, 0);
const timeUntilEvaluation = evaluationTime.getTime() - now.getTime();
// Schedule evaluation
setTimeout(() => {
this.evaluateWarmupProgress();
// Reschedule for next day
this.scheduleDailyEvaluation();
}, timeUntilEvaluation);
logger.log('info', `Scheduled daily warmup evaluation in ${Math.floor(timeUntilEvaluation / 60000)} minutes`);
}
/**
* Evaluate warmup progress and possibly advance stages
*/
private evaluateWarmupProgress(): void {
if (!this.config.autoAdvanceStages) {
logger.log('info', 'Auto-advance stages is disabled, skipping evaluation');
return;
}
// Convert entries to array for compatibility with older JS versions
Array.from(this.warmupStatuses.entries()).forEach(([ip, status]) => {
if (!status.isActive) return;
// Check if current stage duration has elapsed
const currentStage = this.config.stages[status.currentStage - 1];
const now = new Date();
const daysSinceStageStart = Math.floor(
(now.getTime() - status.currentStageStartDate.getTime()) / (24 * 60 * 60 * 1000)
);
if (daysSinceStageStart >= currentStage.durationDays) {
// Check if metrics meet requirements for advancing
const metricsOK = this.checkStageMetrics(status, currentStage);
if (metricsOK) {
// Advance to next stage if not at the final stage
if (status.currentStage < this.config.stages.length) {
this.advanceToNextStage(ip);
} else {
logger.log('info', `IP ${ip} has completed the warmup process`);
}
} else if (this.config.suspendOnMetricDecline) {
// Suspend warmup if metrics don't meet requirements
status.isActive = false;
logger.log('warn', `Suspended warmup for IP ${ip} due to poor metrics`, {
openRate: status.metrics.openRate,
bounceRate: status.metrics.bounceRate,
complaintRate: status.metrics.complaintRate
});
} else {
// Extend current stage if metrics don't meet requirements
logger.log('info', `Extending stage ${status.currentStage} for IP ${ip} due to metrics not meeting requirements`);
}
}
});
// Save updated statuses
this.saveWarmupStatuses();
}
/**
* Check if the current metrics meet the requirements for the stage
* @param status Warmup status to check
* @param stage Stage to check against
* @returns Whether metrics meet requirements
*/
private checkStageMetrics(status: IIPWarmupStatus, stage: IWarmupStage): boolean {
// If no target metrics specified, assume met
if (!stage.targetMetrics) return true;
const metrics = status.metrics;
let meetsRequirements = true;
// Check each metric against requirements
if (stage.targetMetrics.minOpenRate !== undefined &&
metrics.openRate < stage.targetMetrics.minOpenRate) {
meetsRequirements = false;
logger.log('info', `Open rate ${metrics.openRate}% below target ${stage.targetMetrics.minOpenRate}% for IP ${status.ipAddress}`);
}
if (stage.targetMetrics.maxBounceRate !== undefined &&
metrics.bounceRate > stage.targetMetrics.maxBounceRate) {
meetsRequirements = false;
logger.log('info', `Bounce rate ${metrics.bounceRate}% above target ${stage.targetMetrics.maxBounceRate}% for IP ${status.ipAddress}`);
}
if (stage.targetMetrics.maxComplaintRate !== undefined &&
metrics.complaintRate > stage.targetMetrics.maxComplaintRate) {
meetsRequirements = false;
logger.log('info', `Complaint rate ${metrics.complaintRate}% above target ${stage.targetMetrics.maxComplaintRate}% for IP ${status.ipAddress}`);
}
return meetsRequirements;
}
/**
* Advance IP to the next warmup stage
* @param ipAddress IP address to advance
*/
private advanceToNextStage(ipAddress: string): void {
const status = this.warmupStatuses.get(ipAddress);
if (!status) return;
// Store metrics for the completed stage
const completedStage = status.currentStage;
// Advance to next stage
status.currentStage++;
status.currentStageStartDate = new Date();
status.sentInCurrentStage = 0;
// Update allocation
const newStage = this.config.stages[status.currentStage - 1];
status.currentDailyAllocation = newStage.maxDailyVolume;
logger.log('info', `Advanced IP ${ipAddress} to warmup stage ${status.currentStage}`, {
previousStage: completedStage,
newDailyLimit: status.currentDailyAllocation,
durationDays: newStage.durationDays
});
}
/**
* Get warmup status for all IPs or a specific IP
* @param ipAddress Optional specific IP to get status for
* @returns Warmup status information
*/
public getWarmupStatus(ipAddress?: string): IIPWarmupStatus | Map<string, IIPWarmupStatus> {
if (ipAddress) {
return this.warmupStatuses.get(ipAddress);
}
return this.warmupStatuses;
}
/**
* Add a new IP address to the warmup process
* @param ipAddress IP address to add
*/
public addIPToWarmup(ipAddress: string): void {
if (this.config.ipAddresses.includes(ipAddress)) {
logger.log('info', `IP ${ipAddress} is already in warmup`);
return;
}
// Add to configuration
this.config.ipAddresses.push(ipAddress);
// Initialize warmup
this.initializeIPWarmup(ipAddress);
// Initialize counters
this.dailySendCounts.set(ipAddress, 0);
this.hourlySendCounts.set(ipAddress, Array(24).fill(0));
logger.log('info', `Added IP ${ipAddress} to warmup process`);
}
/**
* Remove an IP address from the warmup process
* @param ipAddress IP address to remove
*/
public removeIPFromWarmup(ipAddress: string): void {
const index = this.config.ipAddresses.indexOf(ipAddress);
if (index === -1) {
logger.log('info', `IP ${ipAddress} is not in warmup`);
return;
}
// Remove from configuration
this.config.ipAddresses.splice(index, 1);
// Remove from statuses and counters
this.warmupStatuses.delete(ipAddress);
this.dailySendCounts.delete(ipAddress);
this.hourlySendCounts.delete(ipAddress);
this.saveWarmupStatuses();
logger.log('info', `Removed IP ${ipAddress} from warmup process`);
}
/**
* Update metrics for an IP address
* @param ipAddress IP address to update
* @param metrics New metrics
*/
public updateMetrics(
ipAddress: string,
metrics: { openRate?: number; bounceRate?: number; complaintRate?: number }
): void {
const status = this.warmupStatuses.get(ipAddress);
if (!status) {
logger.log('warn', `Cannot update metrics for IP ${ipAddress} - not in warmup`);
return;
}
// Update metrics
if (metrics.openRate !== undefined) {
status.metrics.openRate = metrics.openRate;
}
if (metrics.bounceRate !== undefined) {
status.metrics.bounceRate = metrics.bounceRate;
}
if (metrics.complaintRate !== undefined) {
status.metrics.complaintRate = metrics.complaintRate;
}
this.saveWarmupStatuses();
logger.log('info', `Updated metrics for IP ${ipAddress}`, {
openRate: status.metrics.openRate,
bounceRate: status.metrics.bounceRate,
complaintRate: status.metrics.complaintRate
});
}
/**
* Record a send event for an IP address
* @param ipAddress IP address used for sending
*/
public recordSend(ipAddress: string): void {
if (!this.config.ipAddresses.includes(ipAddress)) {
logger.log('warn', `Cannot record send for IP ${ipAddress} - not in warmup`);
return;
}
// Increment daily counter
const currentCount = this.dailySendCounts.get(ipAddress) || 0;
this.dailySendCounts.set(ipAddress, currentCount + 1);
// Increment hourly counter
const hourlyCount = this.hourlySendCounts.get(ipAddress) || Array(24).fill(0);
const currentHour = new Date().getHours();
hourlyCount[currentHour]++;
this.hourlySendCounts.set(ipAddress, hourlyCount);
// Update warmup status
const status = this.warmupStatuses.get(ipAddress);
if (status) {
status.sentInCurrentStage++;
status.totalSent++;
}
}
/**
* Check if an IP can send more emails today
* @param ipAddress IP address to check
* @returns Whether the IP can send more emails
*/
public canSendMoreToday(ipAddress: string): boolean {
if (!this.config.enabled) return true;
if (!this.config.ipAddresses.includes(ipAddress)) {
// If not in warmup, assume it can send
return true;
}
const status = this.warmupStatuses.get(ipAddress);
if (!status || !status.isActive) {
return false;
}
const currentCount = this.dailySendCounts.get(ipAddress) || 0;
return currentCount < status.currentDailyAllocation;
}
/**
* Check if an IP can send more emails in the current hour
* @param ipAddress IP address to check
* @returns Whether the IP can send more emails this hour
*/
public canSendMoreThisHour(ipAddress: string): boolean {
if (!this.config.enabled) return true;
if (!this.config.ipAddresses.includes(ipAddress)) {
// If not in warmup, assume it can send
return true;
}
const status = this.warmupStatuses.get(ipAddress);
if (!status || !status.isActive) {
return false;
}
const currentDailyLimit = status.currentDailyAllocation;
const currentHour = new Date().getHours();
const hourlyAllocation = Math.ceil((currentDailyLimit * this.config.hourlyDistribution[currentHour]) / 100);
const hourlyCount = this.hourlySendCounts.get(ipAddress) || Array(24).fill(0);
const currentHourCount = hourlyCount[currentHour];
return currentHourCount < hourlyAllocation;
}
/**
* Get the best IP to use for sending an email
* @param emailInfo Information about the email being sent
* @returns The best IP to use, or null if no suitable IP is available
*/
public getBestIPForSending(emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional?: boolean;
}): string | null {
// If warmup is disabled, return null (caller will use default IP)
if (!this.config.enabled || this.config.ipAddresses.length === 0) {
return null;
}
// Prepare information for allocation policy
const availableIPs = this.config.ipAddresses
.filter(ip => this.canSendMoreToday(ip) && this.canSendMoreThisHour(ip))
.map(ip => {
const status = this.warmupStatuses.get(ip);
return {
ip,
priority: status ? status.currentStage : 1,
capacity: status ? (status.currentDailyAllocation - (this.dailySendCounts.get(ip) || 0)) : 0
};
});
// Use the active allocation policy to determine the best IP
const policy = this.allocationPolicies.get(this.activePolicy);
if (!policy) {
logger.log('warn', `No allocation policy named ${this.activePolicy} found`);
return null;
}
return policy.allocateIP(availableIPs, {
...emailInfo,
isTransactional: emailInfo.isTransactional || false,
isWarmup: true
});
}
/**
* Register a new IP allocation policy
* @param name Policy name
* @param policy Policy implementation
*/
public registerAllocationPolicy(name: string, policy: IIPAllocationPolicy): void {
this.allocationPolicies.set(name, policy);
logger.log('info', `Registered IP allocation policy: ${name}`);
}
/**
* Set the active IP allocation policy
* @param name Policy name
*/
public setActiveAllocationPolicy(name: string): void {
if (!this.allocationPolicies.has(name)) {
logger.log('warn', `No allocation policy named ${name} found`);
return;
}
this.activePolicy = name;
logger.log('info', `Set active IP allocation policy to ${name}`);
}
/**
* Get the total number of stages in the warmup process
* @returns Number of stages
*/
public getStageCount(): number {
return this.config.stages.length;
}
/**
* Load warmup statuses from storage
*/
private loadWarmupStatuses(): void {
try {
const warmupDir = plugins.path.join(paths.dataDir, 'warmup');
plugins.smartfile.fs.ensureDirSync(warmupDir);
const statusFile = plugins.path.join(warmupDir, 'ip_warmup_status.json');
if (plugins.fs.existsSync(statusFile)) {
const data = plugins.fs.readFileSync(statusFile, 'utf8');
const statuses = JSON.parse(data);
for (const status of statuses) {
// Restore date objects
status.startDate = new Date(status.startDate);
status.currentStageStartDate = new Date(status.currentStageStartDate);
status.targetCompletionDate = new Date(status.targetCompletionDate);
this.warmupStatuses.set(status.ipAddress, status);
}
logger.log('info', `Loaded ${this.warmupStatuses.size} IP warmup statuses from storage`);
}
} catch (error) {
logger.log('error', `Failed to load warmup statuses: ${error.message}`, {
stack: error.stack
});
}
}
/**
* Save warmup statuses to storage
*/
private saveWarmupStatuses(): void {
try {
const warmupDir = plugins.path.join(paths.dataDir, 'warmup');
plugins.smartfile.fs.ensureDirSync(warmupDir);
const statusFile = plugins.path.join(warmupDir, 'ip_warmup_status.json');
const statuses = Array.from(this.warmupStatuses.values());
plugins.smartfile.memory.toFsSync(
JSON.stringify(statuses, null, 2),
statusFile
);
logger.log('debug', `Saved ${statuses.length} IP warmup statuses to storage`);
} catch (error) {
logger.log('error', `Failed to save warmup statuses: ${error.message}`, {
stack: error.stack
});
}
}
}
/**
* Policy that balances traffic across IPs based on stage and capacity
*/
class BalancedAllocationPolicy implements IIPAllocationPolicy {
name = 'balanced';
allocateIP(
availableIPs: Array<{ ip: string; priority: number; capacity: number }>,
emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional: boolean;
isWarmup: boolean;
}
): string | null {
if (availableIPs.length === 0) return null;
// Sort IPs by priority (prefer higher stage IPs) and capacity
const sortedIPs = [...availableIPs].sort((a, b) => {
// First by priority (descending)
if (b.priority !== a.priority) {
return b.priority - a.priority;
}
// Then by remaining capacity (descending)
return b.capacity - a.capacity;
});
// Prioritize higher-stage IPs for transactional emails
if (emailInfo.isTransactional) {
return sortedIPs[0].ip;
}
// For marketing emails, spread across IPs with preference for higher stages
// Use weighted random selection based on stage
const totalWeight = sortedIPs.reduce((sum, ip) => sum + ip.priority, 0);
let randomPoint = Math.random() * totalWeight;
for (const ip of sortedIPs) {
randomPoint -= ip.priority;
if (randomPoint <= 0) {
return ip.ip;
}
}
// Fallback to the highest priority IP
return sortedIPs[0].ip;
}
}
/**
* Policy that rotates through IPs in a round-robin fashion
*/
class RoundRobinAllocationPolicy implements IIPAllocationPolicy {
name = 'roundRobin';
private lastIndex = -1;
allocateIP(
availableIPs: Array<{ ip: string; priority: number; capacity: number }>,
emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional: boolean;
isWarmup: boolean;
}
): string | null {
if (availableIPs.length === 0) return null;
// Sort by capacity to ensure even distribution
const sortedIPs = [...availableIPs].sort((a, b) => b.capacity - a.capacity);
// Move to next IP
this.lastIndex = (this.lastIndex + 1) % sortedIPs.length;
return sortedIPs[this.lastIndex].ip;
}
}
/**
* Policy that dedicates specific IPs to specific domains
*/
class DedicatedDomainPolicy implements IIPAllocationPolicy {
name = 'dedicated';
private domainAssignments: Map<string, string> = new Map();
allocateIP(
availableIPs: Array<{ ip: string; priority: number; capacity: number }>,
emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional: boolean;
isWarmup: boolean;
}
): string | null {
if (availableIPs.length === 0) return null;
// Check if we have a dedicated IP for this domain
if (this.domainAssignments.has(emailInfo.domain)) {
const dedicatedIP = this.domainAssignments.get(emailInfo.domain);
// Check if the dedicated IP is in the available list
const isAvailable = availableIPs.some(ip => ip.ip === dedicatedIP);
if (isAvailable) {
return dedicatedIP;
}
}
// If not, assign one and save the assignment
const sortedIPs = [...availableIPs].sort((a, b) => b.capacity - a.capacity);
const assignedIP = sortedIPs[0].ip;
this.domainAssignments.set(emailInfo.domain, assignedIP);
return assignedIP;
}
}