4 Commits
v5.2.6 ... main

Author SHA1 Message Date
04e73c366c v5.3.1
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 5s
Publish to npm / npm-publish (push) Failing after 6s
Release / build-and-release (push) Failing after 4s
2026-03-02 14:06:47 +00:00
8851d61466 fix(mail): add periodic cleanup timers and proper shutdown handling for bounce manager and delivery queue; avoid mutating maps during iteration and prune stale rate-limiter stats to prevent memory growth 2026-03-02 14:06:47 +00:00
b465b01790 v5.3.0
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 5s
Release / build-and-release (push) Failing after 4s
2026-02-26 17:50:52 +00:00
6ed3252485 feat(mailer-bin): use mimalloc as the global allocator for mailer-bin 2026-02-26 17:50:52 +00:00
10 changed files with 116 additions and 24 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## 2026-03-02 - 5.3.1 - fix(mail)
add periodic cleanup timers and proper shutdown handling for bounce manager and delivery queue; avoid mutating maps during iteration and prune stale rate-limiter stats to prevent memory growth
- BounceManager: add cleanupInterval to periodically remove bounce records older than 7 days and log removals; add stop() to clear the interval and prevent leaks
- UnifiedDeliveryQueue: introduce cleanupTimer started in startProcessing() and cleared in stopProcessing(); cleanupOldItems now collects IDs first to avoid mutating the Map while iterating and logs cleaned items; shutdown now relies on stopProcessing to clear timers
- UnifiedRateLimiter: prune stale stats.byIp and stats.byPattern entries for IPs/patterns that no longer have active counters or blocks to reduce memory usage and keep stats accurate
- Auto-cleanup tasks log errors rather than throwing to avoid crashing processing loops
## 2026-02-26 - 5.3.0 - feat(mailer-bin)
use mimalloc as the global allocator for mailer-bin
- Add mimalloc dependency to workspace Cargo.toml
- Enable workspace mimalloc in rust/crates/mailer-bin/Cargo.toml
- Register mimalloc as the #[global_allocator] in mailer-bin/src/main.rs
- Update Cargo.lock with new mimalloc and libmimalloc-sys entries
## 2026-02-26 - 5.2.6 - fix(postinstall) ## 2026-02-26 - 5.2.6 - fix(postinstall)
remove legacy postinstall binary installer and packaging entry remove legacy postinstall binary installer and packaging entry

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartmta", "name": "@push.rocks/smartmta",
"version": "5.2.6", "version": "5.3.1",
"description": "A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.", "description": "A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.",
"keywords": [ "keywords": [
"mta", "mta",

20
rust/Cargo.lock generated
View File

@@ -894,6 +894,16 @@ version = "0.2.181"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
[[package]]
name = "libmimalloc-sys"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.11.0" version = "0.11.0"
@@ -982,6 +992,7 @@ dependencies = [
"mailer-core", "mailer-core",
"mailer-security", "mailer-security",
"mailer-smtp", "mailer-smtp",
"mimalloc",
"rustls", "rustls",
"serde", "serde",
"serde_json", "serde_json",
@@ -1063,6 +1074,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mimalloc"
version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
dependencies = [
"libmimalloc-sys",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"

View File

@@ -32,3 +32,4 @@ clap = { version = "4", features = ["derive"] }
sha2 = "0.10" sha2 = "0.10"
hmac = "0.12" hmac = "0.12"
pbkdf2 = { version = "0.12", default-features = false } pbkdf2 = { version = "0.12", default-features = false }
mimalloc = "0.1"

View File

@@ -22,3 +22,4 @@ dashmap.workspace = true
base64.workspace = true base64.workspace = true
uuid.workspace = true uuid.workspace = true
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
mimalloc = { workspace = true }

View File

@@ -5,6 +5,9 @@
//! 2. **Management mode** (`--management`) — JSON-over-stdin/stdout IPC for //! 2. **Management mode** (`--management`) — JSON-over-stdin/stdout IPC for
//! integration with `@push.rocks/smartrust` from TypeScript //! integration with `@push.rocks/smartrust` from TypeScript
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use dashmap::DashMap; use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartmta', name: '@push.rocks/smartmta',
version: '5.2.6', version: '5.3.1',
description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
} }

View File

@@ -88,7 +88,10 @@ export class BounceManager {
// Store of bounced emails // Store of bounced emails
private bounceStore: BounceRecord[] = []; private bounceStore: BounceRecord[] = [];
// Periodic cleanup timer for old bounce records
private cleanupInterval?: NodeJS.Timeout;
// Cache of recently bounced email addresses to avoid sending to known bad addresses // Cache of recently bounced email addresses to avoid sending to known bad addresses
private bounceCache: LRUCache<string, { private bounceCache: LRUCache<string, {
lastBounce: number; lastBounce: number;
@@ -135,6 +138,15 @@ export class BounceManager {
this.loadSuppressionList().catch(error => { this.loadSuppressionList().catch(error => {
logger.log('error', `Failed to load suppression list on startup: ${error.message}`); logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
}); });
// Start periodic cleanup of old bounce records (every 1 hour, removes records older than 7 days)
this.cleanupInterval = setInterval(() => {
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const removed = this.clearOldBounceRecords(sevenDaysAgo);
if (removed > 0) {
logger.log('info', `Auto-cleanup removed ${removed} old bounce records`);
}
}, 60 * 60 * 1000);
} }
/** /**
@@ -717,7 +729,7 @@ export class BounceManager {
*/ */
public clearOldBounceRecords(olderThan: number): number { public clearOldBounceRecords(olderThan: number): number {
let removed = 0; let removed = 0;
this.bounceStore = this.bounceStore.filter(bounce => { this.bounceStore = this.bounceStore.filter(bounce => {
if (bounce.timestamp < olderThan) { if (bounce.timestamp < olderThan) {
removed++; removed++;
@@ -725,7 +737,17 @@ export class BounceManager {
} }
return true; return true;
}); });
return removed; return removed;
} }
/**
* Stop the bounce manager and clear cleanup timers
*/
public stop(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
}
} }

View File

@@ -78,6 +78,7 @@ export class UnifiedDeliveryQueue extends EventEmitter {
private options: Required<IQueueOptions>; private options: Required<IQueueOptions>;
private queue: Map<string, IQueueItem> = new Map(); private queue: Map<string, IQueueItem> = new Map();
private checkTimer?: NodeJS.Timeout; private checkTimer?: NodeJS.Timeout;
private cleanupTimer?: NodeJS.Timeout;
private stats: IQueueStats; private stats: IQueueStats;
private processing: boolean = false; private processing: boolean = false;
private totalProcessed: number = 0; private totalProcessed: number = 0;
@@ -158,8 +159,19 @@ export class UnifiedDeliveryQueue extends EventEmitter {
if (this.checkTimer) { if (this.checkTimer) {
clearInterval(this.checkTimer); clearInterval(this.checkTimer);
} }
this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval); this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval);
// Start periodic cleanup of delivered/failed items (every 30 minutes)
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(() => {
this.cleanupOldItems(24 * 60 * 60 * 1000).catch((err) => {
logger.log('error', `Auto-cleanup failed: ${err.message}`);
});
}, 30 * 60 * 1000);
this.processing = true; this.processing = true;
this.stats.processingActive = true; this.stats.processingActive = true;
this.emit('processingStarted'); this.emit('processingStarted');
@@ -174,7 +186,11 @@ export class UnifiedDeliveryQueue extends EventEmitter {
clearInterval(this.checkTimer); clearInterval(this.checkTimer);
this.checkTimer = undefined; this.checkTimer = undefined;
} }
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
this.processing = false; this.processing = false;
this.stats.processingActive = false; this.stats.processingActive = false;
this.emit('processingStopped'); this.emit('processingStopped');
@@ -590,19 +606,24 @@ export class UnifiedDeliveryQueue extends EventEmitter {
*/ */
public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> { public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
const cutoff = new Date(Date.now() - maxAge); const cutoff = new Date(Date.now() - maxAge);
let removedCount = 0;
// Collect IDs first to avoid modifying the Map during iteration
// Find old items const idsToRemove: string[] = [];
for (const item of this.queue.values()) { for (const item of this.queue.values()) {
if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) { if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) {
// Remove item idsToRemove.push(item.id);
await this.removeItem(item.id);
removedCount++;
} }
} }
logger.log('info', `Cleaned up ${removedCount} old items`); // Remove collected items
return removedCount; for (const id of idsToRemove) {
await this.removeItem(id);
}
if (idsToRemove.length > 0) {
logger.log('info', `Cleaned up ${idsToRemove.length} old items from delivery queue`);
}
return idsToRemove.length;
} }
/** /**
@@ -611,15 +632,9 @@ export class UnifiedDeliveryQueue extends EventEmitter {
public async shutdown(): Promise<void> { public async shutdown(): Promise<void> {
logger.log('info', 'Shutting down UnifiedDeliveryQueue'); logger.log('info', 'Shutting down UnifiedDeliveryQueue');
// Stop processing // Stop processing (clears both check and cleanup timers)
this.stopProcessing(); this.stopProcessing();
// Clear the check timer to prevent memory leaks
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = undefined;
}
// If using disk storage, make sure all items are persisted // If using disk storage, make sure all items are persisted
if (this.options.storageType === 'disk') { if (this.options.storageType === 'disk') {
const pendingWrites: Promise<void>[] = []; const pendingWrites: Promise<void>[] = [];

View File

@@ -231,7 +231,21 @@ export class UnifiedRateLimiter extends EventEmitter {
this.domainCounters.delete(key); this.domainCounters.delete(key);
} }
} }
// Clean stale stats.byIp entries for IPs that no longer have active counters or blocks
for (const ip of Object.keys(this.stats.byIp)) {
if (!this.ipCounters.has(ip) && !(this.config.blocks && ip in this.config.blocks)) {
delete this.stats.byIp[ip];
}
}
// Clean stale stats.byPattern entries for patterns that no longer have active counters
for (const pattern of Object.keys(this.stats.byPattern)) {
if (!this.patternCounters.has(pattern)) {
delete this.stats.byPattern[pattern];
}
}
// Update statistics // Update statistics
this.updateStats(); this.updateStats();
} }