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

@ -209,10 +209,13 @@ export class BounceManager {
expiresAt?: number; // undefined means permanent
}> = new Map();
private storageManager?: any; // StorageManager instance
constructor(options?: {
retryStrategy?: Partial<RetryStrategy>;
maxCacheSize?: number;
cacheTTL?: number;
storageManager?: any;
}) {
// Set retry strategy with defaults
if (options?.retryStrategy) {
@ -228,8 +231,24 @@ export class BounceManager {
ttl: options?.cacheTTL || 30 * 24 * 60 * 60 * 1000, // 30 days default
});
// Store storage manager reference
this.storageManager = options?.storageManager;
// If no storage manager provided, log warning
if (!this.storageManager) {
console.warn(
'⚠️ WARNING: BounceManager initialized without StorageManager.\n' +
' Bounce data will only be stored to filesystem.\n' +
' Consider passing a StorageManager instance for better storage flexibility.'
);
}
// Load suppression list from storage
this.loadSuppressionList();
// Note: This is async but we can't await in constructor
// The suppression list will be loaded asynchronously
this.loadSuppressionList().catch(error => {
logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
});
}
/**
@ -479,7 +498,7 @@ export class BounceManager {
this.updateBounceCache(bounce);
// Save to permanent storage
this.saveBounceRecord(bounce);
await this.saveBounceRecord(bounce);
// Log hard bounce for monitoring
logger.log('warn', `Hard bounce for ${bounce.recipient}: ${bounce.bounceType}`, {
@ -545,7 +564,10 @@ export class BounceManager {
expiresAt
});
this.saveSuppressionList();
// Save asynchronously without blocking
this.saveSuppressionList().catch(error => {
logger.log('error', `Failed to save suppression list after adding ${email}: ${error.message}`);
});
logger.log('info', `Added ${email} to suppression list`, {
reason,
@ -561,7 +583,10 @@ export class BounceManager {
const wasRemoved = this.suppressionList.delete(email.toLowerCase());
if (wasRemoved) {
this.saveSuppressionList();
// Save asynchronously without blocking
this.saveSuppressionList().catch(error => {
logger.log('error', `Failed to save suppression list after removing ${email}: ${error.message}`);
});
logger.log('info', `Removed ${email} from suppression list`);
}
}
@ -582,7 +607,10 @@ export class BounceManager {
// Check if suppression has expired
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
this.suppressionList.delete(lowercaseEmail);
this.saveSuppressionList();
// Save asynchronously without blocking
this.saveSuppressionList().catch(error => {
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
});
return false;
}
@ -609,7 +637,10 @@ export class BounceManager {
// Check if suppression has expired
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
this.suppressionList.delete(lowercaseEmail);
this.saveSuppressionList();
// Save asynchronously without blocking
this.saveSuppressionList().catch(error => {
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
});
return null;
}
@ -619,13 +650,20 @@ export class BounceManager {
/**
* Save suppression list to disk
*/
private saveSuppressionList(): void {
private async saveSuppressionList(): Promise<void> {
try {
const suppressionData = JSON.stringify(Array.from(this.suppressionList.entries()));
plugins.smartfile.memory.toFsSync(
suppressionData,
plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json')
);
if (this.storageManager) {
// Use storage manager
await this.storageManager.set('/email/bounces/suppression-list.json', suppressionData);
} else {
// Fall back to filesystem
plugins.smartfile.memory.toFsSync(
suppressionData,
plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json')
);
}
} catch (error) {
logger.log('error', `Failed to save suppression list: ${error.message}`);
}
@ -634,14 +672,40 @@ export class BounceManager {
/**
* Load suppression list from disk
*/
private loadSuppressionList(): void {
private async loadSuppressionList(): Promise<void> {
try {
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
let entries = null;
let needsMigration = false;
if (plugins.fs.existsSync(suppressionPath)) {
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
const entries = JSON.parse(data);
if (this.storageManager) {
// Try to load from storage manager first
const suppressionData = await this.storageManager.get('/email/bounces/suppression-list.json');
if (suppressionData) {
entries = JSON.parse(suppressionData);
} else {
// Check if data exists in filesystem and migrate
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
if (plugins.fs.existsSync(suppressionPath)) {
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
entries = JSON.parse(data);
needsMigration = true;
logger.log('info', 'Migrating suppression list from filesystem to StorageManager');
}
}
} else {
// No storage manager, use filesystem directly
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
if (plugins.fs.existsSync(suppressionPath)) {
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
entries = JSON.parse(data);
}
}
if (entries) {
this.suppressionList = new Map(entries);
// Clean expired entries
@ -655,9 +719,9 @@ export class BounceManager {
}
}
if (expiredCount > 0) {
if (expiredCount > 0 || needsMigration) {
logger.log('info', `Cleaned ${expiredCount} expired entries from suppression list`);
this.saveSuppressionList();
await this.saveSuppressionList();
}
logger.log('info', `Loaded ${this.suppressionList.size} entries from suppression list`);
@ -671,21 +735,28 @@ export class BounceManager {
* Save bounce record to disk
* @param bounce Bounce record to save
*/
private saveBounceRecord(bounce: BounceRecord): void {
private async saveBounceRecord(bounce: BounceRecord): Promise<void> {
try {
const bounceData = JSON.stringify(bounce);
const bouncePath = plugins.path.join(
paths.dataDir,
'emails',
'bounces',
`${bounce.id}.json`
);
const bounceData = JSON.stringify(bounce, null, 2);
// Ensure directory exists
const bounceDir = plugins.path.join(paths.dataDir, 'emails', 'bounces');
plugins.smartfile.fs.ensureDirSync(bounceDir);
plugins.smartfile.memory.toFsSync(bounceData, bouncePath);
if (this.storageManager) {
// Use storage manager
await this.storageManager.set(`/email/bounces/records/${bounce.id}.json`, bounceData);
} else {
// Fall back to filesystem
const bouncePath = plugins.path.join(
paths.dataDir,
'emails',
'bounces',
`${bounce.id}.json`
);
// Ensure directory exists
const bounceDir = plugins.path.join(paths.dataDir, 'emails', 'bounces');
plugins.smartfile.fs.ensureDirSync(bounceDir);
plugins.smartfile.memory.toFsSync(bounceData, bouncePath);
}
} catch (error) {
logger.log('error', `Failed to save bounce record: ${error.message}`);
}