feat(integration): components now play nicer with each other

This commit is contained in:
2025-05-30 05:30:06 +00:00
parent 2c244c4a9a
commit 40db395591
19 changed files with 2849 additions and 264 deletions

View File

@ -171,12 +171,14 @@ export class SenderReputationMonitor {
private reputationData: Map<string, IDomainReputationMetrics> = new Map();
private updateTimer: NodeJS.Timeout = null;
private isInitialized: boolean = false;
private storageManager?: any; // StorageManager instance
/**
* Constructor for SenderReputationMonitor
* @param config Configuration options
* @param storageManager Optional StorageManager instance
*/
constructor(config: IReputationMonitorConfig = {}) {
constructor(config: IReputationMonitorConfig = {}, storageManager?: any) {
// Merge with default config
this.config = {
...DEFAULT_CONFIG,
@ -191,18 +193,34 @@ export class SenderReputationMonitor {
}
};
// Initialize
this.initialize();
this.storageManager = storageManager;
// If no storage manager provided, log warning
if (!storageManager) {
logger.log('warn',
'⚠️ WARNING: SenderReputationMonitor initialized without StorageManager.\n' +
' Reputation data will only be stored to filesystem.\n' +
' Consider passing a StorageManager instance for better storage flexibility.'
);
}
// Initialize (async, but we don't await here to avoid blocking constructor)
this.initialize().catch(error => {
logger.log('error', `Failed to initialize SenderReputationMonitor: ${error.message}`, {
stack: error.stack
});
});
}
/**
* Get the singleton instance
* @param config Configuration options
* @param storageManager Optional StorageManager instance
* @returns Singleton instance
*/
public static getInstance(config: IReputationMonitorConfig = {}): SenderReputationMonitor {
public static getInstance(config: IReputationMonitorConfig = {}, storageManager?: any): SenderReputationMonitor {
if (!SenderReputationMonitor.instance) {
SenderReputationMonitor.instance = new SenderReputationMonitor(config);
SenderReputationMonitor.instance = new SenderReputationMonitor(config, storageManager);
}
return SenderReputationMonitor.instance;
}
@ -210,7 +228,7 @@ export class SenderReputationMonitor {
/**
* Initialize the reputation monitor
*/
private initialize(): void {
private async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
@ -219,7 +237,7 @@ export class SenderReputationMonitor {
if (!isTestEnvironment) {
// Load existing reputation data
this.loadReputationData();
await this.loadReputationData();
}
// Initialize data for any new domains
@ -354,7 +372,7 @@ export class SenderReputationMonitor {
}
// Save all updated data
this.saveReputationData();
await this.saveReputationData();
logger.log('info', 'Completed reputation metrics update for all domains');
}
@ -977,7 +995,11 @@ export class SenderReputationMonitor {
// Skip in test environment
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (!isTestEnvironment && Math.random() < 0.01) { // ~1% chance to save on each event
this.saveReputationData();
this.saveReputationData().catch(error => {
logger.log('error', `Failed to save reputation data: ${error.message}`, {
stack: error.stack
});
});
}
}
@ -1037,7 +1059,11 @@ export class SenderReputationMonitor {
this.config.domains.push(domain);
this.initializeDomainData(domain);
this.saveReputationData();
this.saveReputationData().catch(error => {
logger.log('error', `Failed to save reputation data after adding domain: ${error.message}`, {
stack: error.stack
});
});
logger.log('info', `Added ${domain} to reputation monitoring`);
}
@ -1055,7 +1081,11 @@ export class SenderReputationMonitor {
this.config.domains.splice(index, 1);
this.reputationData.delete(domain);
this.saveReputationData();
this.saveReputationData().catch(error => {
logger.log('error', `Failed to save reputation data after removing domain: ${error.message}`, {
stack: error.stack
});
});
logger.log('info', `Removed ${domain} from reputation monitoring`);
}
@ -1063,7 +1093,7 @@ export class SenderReputationMonitor {
/**
* Load reputation data from storage
*/
private loadReputationData(): void {
private async loadReputationData(): Promise<void> {
// Skip loading in test environment to prevent file system race conditions
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (isTestEnvironment) {
@ -1071,32 +1101,98 @@ export class SenderReputationMonitor {
}
try {
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);
const dataFile = plugins.path.join(reputationDir, 'domain_reputation.json');
if (plugins.fs.existsSync(dataFile)) {
const data = plugins.fs.readFileSync(dataFile, 'utf8');
const reputationEntries = JSON.parse(data);
for (const entry of reputationEntries) {
// Restore Date objects
entry.lastUpdated = new Date(entry.lastUpdated);
for (const listing of entry.blocklist.activeListings) {
listing.listedSince = new Date(listing.listedSince);
// Try to load from storage manager first
if (this.storageManager) {
try {
const data = await this.storageManager.get('/email/reputation/domain-reputation.json');
if (data) {
const reputationEntries = JSON.parse(data);
for (const entry of reputationEntries) {
// Restore Date objects
entry.lastUpdated = new Date(entry.lastUpdated);
for (const listing of entry.blocklist.activeListings) {
listing.listedSince = new Date(listing.listedSince);
}
for (const delisting of entry.blocklist.recentDelistings) {
delisting.listedFrom = new Date(delisting.listedFrom);
delisting.listedTo = new Date(delisting.listedTo);
}
this.reputationData.set(entry.domain, entry);
}
logger.log('info', `Loaded reputation data for ${this.reputationData.size} domains from StorageManager`);
return;
}
for (const delisting of entry.blocklist.recentDelistings) {
delisting.listedFrom = new Date(delisting.listedFrom);
delisting.listedTo = new Date(delisting.listedTo);
}
this.reputationData.set(entry.domain, entry);
} catch (error) {
// Fall through to filesystem migration check
}
logger.log('info', `Loaded reputation data for ${this.reputationData.size} domains`);
// Check if data exists in filesystem and migrate it to storage manager
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
const dataFile = plugins.path.join(reputationDir, 'domain_reputation.json');
if (plugins.fs.existsSync(dataFile)) {
const data = plugins.fs.readFileSync(dataFile, 'utf8');
const reputationEntries = JSON.parse(data);
for (const entry of reputationEntries) {
// Restore Date objects
entry.lastUpdated = new Date(entry.lastUpdated);
for (const listing of entry.blocklist.activeListings) {
listing.listedSince = new Date(listing.listedSince);
}
for (const delisting of entry.blocklist.recentDelistings) {
delisting.listedFrom = new Date(delisting.listedFrom);
delisting.listedTo = new Date(delisting.listedTo);
}
this.reputationData.set(entry.domain, entry);
}
// Migrate to storage manager
logger.log('info', `Migrating reputation data for ${this.reputationData.size} domains from filesystem to StorageManager`);
await this.storageManager.set(
'/email/reputation/domain-reputation.json',
JSON.stringify(Array.from(this.reputationData.values()), null, 2)
);
logger.log('info', `Loaded and migrated reputation data for ${this.reputationData.size} domains`);
}
} else {
// No storage manager, use filesystem directly
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);
const dataFile = plugins.path.join(reputationDir, 'domain_reputation.json');
if (plugins.fs.existsSync(dataFile)) {
const data = plugins.fs.readFileSync(dataFile, 'utf8');
const reputationEntries = JSON.parse(data);
for (const entry of reputationEntries) {
// Restore Date objects
entry.lastUpdated = new Date(entry.lastUpdated);
for (const listing of entry.blocklist.activeListings) {
listing.listedSince = new Date(listing.listedSince);
}
for (const delisting of entry.blocklist.recentDelistings) {
delisting.listedFrom = new Date(delisting.listedFrom);
delisting.listedTo = new Date(delisting.listedTo);
}
this.reputationData.set(entry.domain, entry);
}
logger.log('info', `Loaded reputation data for ${this.reputationData.size} domains from filesystem`);
}
}
} catch (error) {
logger.log('error', `Failed to load reputation data: ${error.message}`, {
@ -1108,7 +1204,7 @@ export class SenderReputationMonitor {
/**
* Save reputation data to storage
*/
private saveReputationData(): void {
private async saveReputationData(): Promise<void> {
// Skip saving in test environment to prevent file system race conditions
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (isTestEnvironment) {
@ -1116,18 +1212,29 @@ export class SenderReputationMonitor {
}
try {
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);
const dataFile = plugins.path.join(reputationDir, 'domain_reputation.json');
const reputationEntries = Array.from(this.reputationData.values());
plugins.smartfile.memory.toFsSync(
JSON.stringify(reputationEntries, null, 2),
dataFile
);
logger.log('debug', `Saved reputation data for ${reputationEntries.length} domains`);
// Save to storage manager if available
if (this.storageManager) {
await this.storageManager.set(
'/email/reputation/domain-reputation.json',
JSON.stringify(reputationEntries, null, 2)
);
logger.log('debug', `Saved reputation data for ${reputationEntries.length} domains to StorageManager`);
} else {
// No storage manager, use filesystem directly
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);
const dataFile = plugins.path.join(reputationDir, 'domain_reputation.json');
plugins.smartfile.memory.toFsSync(
JSON.stringify(reputationEntries, null, 2),
dataFile
);
logger.log('debug', `Saved reputation data for ${reputationEntries.length} domains to filesystem`);
}
} catch (error) {
logger.log('error', `Failed to save reputation data: ${error.message}`, {
stack: error.stack