@push.rocks/smartmta 6.4.1 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +11 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/logger.d.ts +2 -0
- package/dist_ts/logger.js +8 -3
- package/dist_ts/mail/delivery/classes.delivery.system.js +16 -2
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +14 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +24 -1
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +5 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +96 -47
- package/package.json +1 -1
- package/readme.md +24 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/logger.ts +8 -2
- package/ts/mail/delivery/classes.delivery.system.ts +17 -1
- package/ts/mail/routing/classes.unified.email.server.ts +26 -0
- package/ts/security/classes.rustsecuritybridge.ts +97 -49
|
@@ -406,7 +406,9 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
406
406
|
private _state: BridgeState = BridgeState.Idle;
|
|
407
407
|
private _restartAttempts = 0;
|
|
408
408
|
private _restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
409
|
+
private _spawnInFlight = false;
|
|
409
410
|
private _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
411
|
+
private _healthCheckStrikes = 0;
|
|
410
412
|
private _deliberateStop = false;
|
|
411
413
|
private _smtpServerConfig: ISmtpServerConfig | null = null;
|
|
412
414
|
|
|
@@ -454,12 +456,15 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
454
456
|
|
|
455
457
|
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
|
456
458
|
this._running = false;
|
|
457
|
-
logger.log('
|
|
459
|
+
logger.log('error', `Rust security bridge exited (code=${code}, signal=${signal})`);
|
|
458
460
|
|
|
459
461
|
if (this._deliberateStop) {
|
|
460
462
|
this.setState(BridgeState.Stopped);
|
|
461
|
-
} else if (this._state
|
|
462
|
-
//
|
|
463
|
+
} else if (!this._restartTimer && !this._spawnInFlight && this._state !== BridgeState.Starting) {
|
|
464
|
+
// Any unexpected exit triggers recovery, regardless of the state we
|
|
465
|
+
// were in. Exits during Starting resolve through start() itself;
|
|
466
|
+
// exits while a restart is already pending/spawning are absorbed by
|
|
467
|
+
// the in-flight recovery cycle.
|
|
463
468
|
this.attemptRestart();
|
|
464
469
|
}
|
|
465
470
|
});
|
|
@@ -540,7 +545,7 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
540
545
|
case BridgeState.Restarting:
|
|
541
546
|
throw new Error('Rust bridge is restarting after a crash. Commands will resume once it recovers.');
|
|
542
547
|
case BridgeState.Failed:
|
|
543
|
-
throw new Error('Rust bridge
|
|
548
|
+
throw new Error('Rust bridge is in degraded state after repeated crashes. Recovery attempts continue in the background.');
|
|
544
549
|
case BridgeState.Stopped:
|
|
545
550
|
throw new Error('Rust bridge has been stopped. Call start() to restart it.');
|
|
546
551
|
default:
|
|
@@ -563,6 +568,11 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
563
568
|
|
|
564
569
|
this._deliberateStop = false;
|
|
565
570
|
this._restartAttempts = 0;
|
|
571
|
+
// An explicit start supersedes any pending background recovery attempt
|
|
572
|
+
if (this._restartTimer) {
|
|
573
|
+
clearTimeout(this._restartTimer);
|
|
574
|
+
this._restartTimer = null;
|
|
575
|
+
}
|
|
566
576
|
this.setState(BridgeState.Starting);
|
|
567
577
|
|
|
568
578
|
try {
|
|
@@ -619,69 +629,97 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
619
629
|
const config = RustSecurityBridge._resilienceConfig;
|
|
620
630
|
this._restartAttempts++;
|
|
621
631
|
|
|
622
|
-
|
|
623
|
-
|
|
632
|
+
const exhausted = this._restartAttempts > config.maxRestartAttempts;
|
|
633
|
+
if (exhausted) {
|
|
634
|
+
// Degraded, but never terminal: a mail backend that silently stays dead
|
|
635
|
+
// is worse than one that keeps retrying at the slow cadence. The Failed
|
|
636
|
+
// state signals the degradation to embedders (stateChange event) while
|
|
637
|
+
// recovery attempts continue at restartBackoffMaxMs.
|
|
638
|
+
logger.log('error', `Rust bridge exhausted ${config.maxRestartAttempts} fast restart attempts — continuing in degraded slow-retry every ${config.restartBackoffMaxMs}ms (attempt ${this._restartAttempts})`);
|
|
624
639
|
this.setState(BridgeState.Failed);
|
|
625
|
-
|
|
640
|
+
} else {
|
|
641
|
+
this.setState(BridgeState.Restarting);
|
|
626
642
|
}
|
|
627
|
-
|
|
628
|
-
this.setState(BridgeState.Restarting);
|
|
629
643
|
this.stopHealthCheck();
|
|
630
644
|
|
|
631
|
-
const delay =
|
|
632
|
-
config.
|
|
633
|
-
|
|
634
|
-
|
|
645
|
+
const delay = exhausted
|
|
646
|
+
? config.restartBackoffMaxMs
|
|
647
|
+
: Math.min(
|
|
648
|
+
config.restartBackoffBaseMs * Math.pow(2, this._restartAttempts - 1),
|
|
649
|
+
config.restartBackoffMaxMs,
|
|
650
|
+
);
|
|
635
651
|
|
|
636
|
-
logger.log('info', `Rust bridge restart attempt ${this._restartAttempts}
|
|
652
|
+
logger.log('info', `Rust bridge restart attempt ${this._restartAttempts} in ${delay}ms`);
|
|
637
653
|
|
|
638
|
-
this._restartTimer = setTimeout(
|
|
654
|
+
this._restartTimer = setTimeout(() => {
|
|
639
655
|
this._restartTimer = null;
|
|
656
|
+
void this.executeRestartAttempt();
|
|
657
|
+
}, delay);
|
|
658
|
+
}
|
|
640
659
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
660
|
+
private async executeRestartAttempt(): Promise<void> {
|
|
661
|
+
// Guard: if stop() was called while we were waiting, don't restart
|
|
662
|
+
if (this._deliberateStop) {
|
|
663
|
+
this.setState(BridgeState.Stopped);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
this._spawnInFlight = true;
|
|
668
|
+
try {
|
|
669
|
+
const ok = await this.bridge.spawn();
|
|
670
|
+
this._running = ok;
|
|
671
|
+
if (!ok) {
|
|
672
|
+
throw new Error('spawn returned false');
|
|
645
673
|
}
|
|
646
674
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
this.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
await this.restoreAfterRestart();
|
|
657
|
-
} else {
|
|
658
|
-
logger.log('warn', 'Rust bridge restart failed (spawn returned false)');
|
|
659
|
-
this.attemptRestart();
|
|
675
|
+
const restored = await this.restoreAfterRestart();
|
|
676
|
+
if (!restored) {
|
|
677
|
+
// A bridge without its SMTP listener must not report Running — kill
|
|
678
|
+
// and go through another restart cycle instead of sitting half-alive.
|
|
679
|
+
logger.log('error', 'Rust bridge restarted but the SMTP server could not be restored — killing process to retry');
|
|
680
|
+
try {
|
|
681
|
+
this.bridge.kill();
|
|
682
|
+
} catch {
|
|
683
|
+
// Already dead
|
|
660
684
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
this.attemptRestart();
|
|
685
|
+
this._running = false;
|
|
686
|
+
throw new Error('SMTP server restore failed');
|
|
664
687
|
}
|
|
665
|
-
|
|
688
|
+
|
|
689
|
+
logger.log('info', 'Rust bridge restarted successfully');
|
|
690
|
+
this._restartAttempts = 0;
|
|
691
|
+
this.setState(BridgeState.Running);
|
|
692
|
+
this.startHealthCheck();
|
|
693
|
+
} catch (err) {
|
|
694
|
+
logger.log('error', `Rust bridge restart failed: ${(err as Error).message}`);
|
|
695
|
+
this.attemptRestart();
|
|
696
|
+
} finally {
|
|
697
|
+
this._spawnInFlight = false;
|
|
698
|
+
}
|
|
666
699
|
}
|
|
667
700
|
|
|
668
701
|
/**
|
|
669
702
|
* Restore state after a successful restart:
|
|
670
703
|
* - Re-send startSmtpServer command if the SMTP server was running
|
|
704
|
+
*
|
|
705
|
+
* @returns `true` if there was nothing to restore or the restore succeeded.
|
|
671
706
|
*/
|
|
672
|
-
private async restoreAfterRestart(): Promise<
|
|
673
|
-
if (this._smtpServerConfig) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
} catch (err) {
|
|
683
|
-
logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`);
|
|
707
|
+
private async restoreAfterRestart(): Promise<boolean> {
|
|
708
|
+
if (!this._smtpServerConfig) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
logger.log('info', 'Restoring SMTP server after bridge restart');
|
|
713
|
+
const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig);
|
|
714
|
+
if (result?.started) {
|
|
715
|
+
logger.log('info', 'SMTP server restored after bridge restart');
|
|
716
|
+
return true;
|
|
684
717
|
}
|
|
718
|
+
logger.log('warn', 'SMTP server failed to restore after bridge restart');
|
|
719
|
+
return false;
|
|
720
|
+
} catch (err) {
|
|
721
|
+
logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`);
|
|
722
|
+
return false;
|
|
685
723
|
}
|
|
686
724
|
}
|
|
687
725
|
|
|
@@ -693,6 +731,7 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
693
731
|
this.stopHealthCheck();
|
|
694
732
|
const config = RustSecurityBridge._resilienceConfig;
|
|
695
733
|
|
|
734
|
+
this._healthCheckStrikes = 0;
|
|
696
735
|
this._healthCheckTimer = setInterval(async () => {
|
|
697
736
|
if (this._state !== BridgeState.Running || !this._running) {
|
|
698
737
|
return;
|
|
@@ -707,8 +746,17 @@ export class RustSecurityBridge extends EventEmitter {
|
|
|
707
746
|
if (!(res as any)?.pong) {
|
|
708
747
|
throw new Error('Health check: unexpected ping response');
|
|
709
748
|
}
|
|
749
|
+
this._healthCheckStrikes = 0;
|
|
710
750
|
} catch (err) {
|
|
711
|
-
|
|
751
|
+
// Two consecutive failures before killing: a single missed ping can
|
|
752
|
+
// be momentary load, and killing mid-transaction is expensive.
|
|
753
|
+
this._healthCheckStrikes++;
|
|
754
|
+
if (this._healthCheckStrikes < 2) {
|
|
755
|
+
logger.log('warn', `Rust bridge health check failed (strike ${this._healthCheckStrikes}/2): ${(err as Error).message}`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
logger.log('error', `Rust bridge health check failed twice in a row: ${(err as Error).message}. Killing process to trigger restart.`);
|
|
759
|
+
this._healthCheckStrikes = 0;
|
|
712
760
|
try {
|
|
713
761
|
this.bridge.kill();
|
|
714
762
|
} catch {
|